Catégorie : Projets

Projets

NsCinéma – La base de données

Trouver le film idéal à regarder ? C’est ce que vous proposera cette base de donnée de films utilisant tkinter.
Cette collection comportant des films de tout genre regroupe de nombreux films en les notant, très pratique pour dénicher les meilleurs films.

Le thème

Nous avons choisi le cinéma comme thème pour notre base de données car nous avons tous les deux cette passion en commun. En effet, en plus du fait que nous aimons le cinéma, l’idée de choisir des films pour constituer notre base de données semble correspondre aux consignes et nous a assez vite donné des idées de fonctionnalités, notamment avec les filtres de recherche que nous avons mis en place.

Le code

Tout d’abord nous avons importé toutes les bibliothèques nécessaires au bon fonctionnement de la base de donnée.

from tkinter import *
import tkinter as tk
import sqlite3
from tkinter import messagebox

Ici Tkinter va permettre d’avoir un rendu graphique de la base de données, le tutoriel présent sur le site nsi.xyz nous a fortement aidé (tutoriel).

Enfin Sqlite3 permet de nous connecter a la base de donné (DBbrowser)

Page d’accueil

Pour la page d’accueil, nous avons donc décidé de rester simple. En effet, sur cette page, on y trouve le nom de notre projet, « NsCinéma ». Ensuite, il y a deux boutons : tout d’abord, un bouton qui permet de consulter la base de données sans la modifier, avec un système de filtres et de défilement. Ensuite, un deuxième bouton permet de modifier la base de données, ce qui inclut la création d’une table Film, la suppression de la table, l’ajout de films, ainsi que la possibilité de modifier un film en cas d’erreur lors de l’ajout.

Ainsi, afin que les boutons puissent fonctionner correctement avec l’ouverture d’une nouvelle page, il était nécessaire d’implémenter une fonction permettant de créer la page « Modifier la base de donnée »

Fonction create permettant d’acceder a la page modifier la base de donnée :

def create():
    glodef create():
    global nouvelle_fenetre
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    def retour():
        nouvelle_fenetre.destroy()

    #titre
    titre = Label(nouvelle_fenetre, text="Mise a jour de la base de donnée", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)
    
    #creer la table 
    bouton_creer_table = Button(nouvelle_fenetre, text="Créer Table Film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=create_table, width=60, height=3)
    bouton_creer_table.place(x=250, y=300)

    #Bouton supprimer 
    bouton_supprimer_table = Button(nouvelle_fenetre, text="Supprimer Table Film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=supprimer_table, width=60, height=3)
    bouton_supprimer_table.place(x=250, y=400)

    #bouton ajouter
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Ajouter un film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=page_ajouter_film, width=60, height=3)
    bouton_ajouter_film.place(x=250, y=500)
    
    #bouton modifier
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Modifier la table", font=("Consolas",13), bg="#1d1e2e", fg="white", command=update, width=60, height=3)
    bouton_ajouter_film.place(x=250, y=600)
    
    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)
    
    
    
    connection.commit()
    curseur.close()
    connection.close()

Dans cette fonction, nous pouvons observer la présence de la fonction ‘retour’ permettant de revenir en arrière en supprimant la page actuelle.

Bouton :

bouton1 = Button(screen, text="Consulter la base de donnée ", font=("Consolas",13), bg="#272936", fg="white", command= create, width=60, height=3)
bouton1.place(x=250, y=300)

bouton2 = Button(screen, text="Mettre a jour la base de donnée ", font=("Consolas",13), bg="#272936", fg="white", command= create, width=60, height=3)
bouton2.place(x=250, y =400)

Consultation de la base de donnée

Passons maintenons au code permettant de consulter cette base donnée. On créée donc une nouvelle fenêtre constituée de deux parties : une pour appliquer des filtres sur les films et une pour voir les films.

Comme on peut le constater dans le screen ci-dessous.

Vous constatez donc une boîte où l’on peut consulter les films de la base, et au-dessus de celle-ci, un bloc comportant un menu déroulant pour les filtres.

On pourra noter l’utilisation d’un bouton retour pour revenir à la page d’accueil et d’un bouton actualise qui nous servira plus tard pour ajouter/supprimer des films.

Passons plus en détail le code.

nouvelle_fenetre = tk.Tk()
    nouvelle_fenetre.title("NsCinema")
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")

main_frame = Frame(nouvelle_fenetre, bg="#0e101c")
main_frame.place(x=80, y=300, width=920, height=420)

canvas = Canvas(main_frame, bg="#0e101c")
canvas.pack(side=LEFT, fill=BOTH, expand=YES)

scrollbar = Scrollbar(main_frame, command=canvas.yview)
scrollbar.pack(side=RIGHT, fill=Y)

canvas.configure(yscrollcommand=scrollbar.set)

boitefilm = Frame(canvas, bg="#1d1e2e")
canvas.create_window((0, 0), window=boitefilm, anchor='nw')
    
bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
bouton_retour.place(x=80, y=120)
    
bouton_actualise = Button(nouvelle_fenetre, text="Actualise", font=("Consolas",13), bg="#1d1e2e", fg="white", command=actualise, width=20, height=2)
bouton_actualise.place(x=800, y=120)
    

boitefilm correspond à la fenêtre (c’est une frame) où l’on, comme on l’à déjà dit, consulte les films. Elle est dans un canevas (ici canvas) qui lui va regrouper tous les éléments graphiques dont boitefilm et notamment la scrollbar qui va permettre de défiler l’axe des ordonnées de canvas : command=canvas.yview.

def etoile(n):
        c = n
        note = ""
        while c != 0:
            note += "★"
            c -= 1
            
        while (5-n) != 0:
            note += "☆"
            n += 1
        return note

    def titre():
        t_nom = Label(boitefilm, text="Nom", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_genre = Label(boitefilm, text="Genre", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_realisateur = Label(boitefilm, text="Réalisateur", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_annee_pub = Label(boitefilm, text="Année", font=("Consolas", 14), bg="#1d1e2e", fg="black")
        t_note = Label(boitefilm, text="Note", font=("Consolas", 13), bg="#1d1e2e", fg="black")

        t_nom.grid(row=0, column=0, sticky=W, pady=21, padx=20)
        t_genre.grid(row=0, column=1, sticky=W, pady=21, padx=5)
        t_realisateur.grid(row=0, column=2, sticky=W, pady=21, padx=5)
        t_annee_pub.grid(row=0, column=3, sticky=W, pady=21, padx=10)
        t_note.grid(row=0, column=4, sticky=W, pady=21, padx=12)
    
            
    def note(n):
        connection = sqlite3.connect('base_films.db')
        curseur = connection.cursor()

        curseur.execute("SELECT * FROM Film WHERE note=?", (n,))
        films = curseur.fetchall()

        connection.commit()
        curseur.close()
        connection.close()

        return films

Ici nous avons trois fonctions qui ont des tâches spécifiques :

  • etoile(n) : c’est une fonction qui va retourner, en fonction du paramètre n, une chaîne de caractère avec n étoiles « ★ » et (5-n) étoiles « ☆ ». Ce qui nous donnes au final un note sur 5 représentée avec des étoiles.
  • titre() : cette fonction va tout simplement permettre l’affichage des titres (avec l’objet Label de tkinter qui permet d’afficher du texte) à la première ligne de la grille avec le paramètre row=0.
  • note() : C’est une fonction qui se connecte à une notre de données contenant les informations sur les films. Elle récupère tous les films ayant une note égale à n et les renvoie. Ici, films = curseur.fetchall() permet de récupérer dans la variable films la requête SQL.

Passons maitenant à la fonction la plus importante de cette partie : ajoute_film()

    
    def ajoute_film(nom, genre, realisateur, annee_pub, note):
        global row_compteur
        connection = sqlite3.connect('base_films.db')
        curseur = connection.cursor()

        truc = (nom, genre, realisateur, annee_pub, note)
        curseur.execute('''CREATE TABLE IF NOT EXISTS Film (
                        id_film INTEGER PRIMARY KEY AUTOINCREMENT,
                        nom TEXT,
                        genre TEXT,
                        realisateur TEXT,
                        annee_pub INTEGER,
                        note REAL
                    )''')
        

        curseur.execute("INSERT INTO Film (nom, genre, realisateur, annee_pub, note) VALUES (?,?,?,?,?)", truc)
        
        

        connection.commit()
        curseur.close()
        connection.close()

        b_nom = Label(boitefilm, text="%s" %(nom), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_genre = Label(boitefilm, text="%s" %(genre), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_realisateur = Label(boitefilm, text="%s" %(realisateur), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_annee_pub = Label(boitefilm, text="%s" %(annee_pub), font=("Consolas", 14), bg="#1d1e2e", fg="white")
        b_note = Label(boitefilm, text="%s" %(etoile(note)), font=("Consolas", 13), bg="#1d1e2e", fg="white")

        b_nom.grid(row=row_compteur, column=0, sticky=W, pady=21, padx=30)
        b_genre.grid(row=row_compteur, column=1, sticky=W, pady=21, padx=15)
        b_realisateur.grid(row=row_compteur, column=2, sticky=W, pady=21, padx=10)
        b_annee_pub.grid(row=row_compteur, column=3, sticky=W, pady=21, padx=10)
        b_note.grid(row=row_compteur, column=4, sticky=W, pady=21, padx=20)
		
        row_compteur += 1


Cette fonction, ajoute_film, ajoute les informations d’un film (nom, genre, réalisateur, année de publication, note) à une base de données SQLite. Elle crée également des objets Label pour afficher ces informations dans une grille, en utilisant la variable row_compteur (c’est une globale) pour déterminer la ligne à laquelle il faut placer ces dernières.

En somme, tel un plat de lasagnes, on ajoute les films ligne après ligne.

Nous utilisons donc cette fonction pour construire notre base de données à l’aide du bout de code suivant et d’une bonne veille boucle for i in range() :

data = [
        ("American Sniper", "Biopic", "Clint Eastwood", 2015, 4),
        ("The Fabel Man", "Biopic", "Steven Spielberg", 2022, 3),
        ("Truman Show", "Science-Fiction", "Peter Weir", 1998, 4),
        ("Prisoners", "Drame", "Denis Villeneuve", 2013, 4),
        ("Very Bad Trip 1", "Comédie", "Todd Phillips", 2009, 3),
        ("Very Bad Trip 2", "Comédie", "Todd Phillips", 2011, 3),
        ("Very Bad Trip 3", "Comédie", "Todd Phillips", 2013, 2),
        ("Ocean Eleven", "Action", "Steven Soderbergh", 2001, 3),
        ("Ocean Twelve", "Action", "Steven Soderbergh", 2004, 2),
        ("Ocean Thirteen", "Action", "Steven Soderbergh", 2007, 3),
        ("La La Land", "Drame", "Damien Chazelle", 2016, 4),
        ("Fury", "Action", "David Ayer", 2014, 3),
        ("Voyage de Chihiro", "Animation", "Hayao Miyazaki", 2001, 5),
        ("Le Château Ambulant", "Animation", "Hayao Miyazaki", 2004, 4),
        ("Goodfellas", "Drame", "Martin Scorsese", 1990, 3),
        ("A Man Called Otto", "Drame", "Marc Forster", 2022, 3),
        ("Elvis", "Biopic", "John Carpenter", 1979, 3),
        ("127 Hours", "Biopic", "Danny Boyle", 2010, 3),
        ("Ad Astra", "Science-Fiction", "James Gray", 2019, 3),
        ("Life: Origine Inconnue", "Science-Fiction", "Daniel Espinosa", 2017, 3),
        ("The Hobbit 1", "Action", "Peter Jackson", 2012, 4),
        ("The Hobbit 2", "Action", "Peter Jackson", 2013, 4),
        ("The Hobbit 3", "Action", "Peter Jackson", 2014, 4),
        ("The Lord of the Rings 1", "Action", "Peter Jackson", 2001, 4),
        ("The Lord of the Rings 2", "Action", "Peter Jackson", 2002, 4),
        ("The Lord of the Rings 3", "Action", "Peter Jackson", 2003, 5),
        ("The Imitation Game", "Biopic", "Morten Tyldum", 2014, 3),
        ("Seul au monde", "Drame", "Robert Zemeckis", 2000, 4),
        ("Nightcrawler", "Thriller", "Dan Gilroy", 2014, 4),
        ("Marriage Story", "Drame", "Noah Baumbach", 2019, 3),
        ("First Man", "Biopic", "Damien Chazelle", 2018, 3),
        ("L'Intervention", "Action", "Fred Grivois", 2019, 4),
        ("Stand by Me", "Drame", "Rob Reiner", 1986, 4),
        ("Middle Men", "Drame", "George Gallo", 2009, 3),
        ("Happiness Therapy", "Comédie", "David O. Russell", 2012, 4),
        ("The Interview", "Comédie", "Seth Rogen", 2014, 1),
        ("Joy", "Drame", "David O. Russell", 2015, 3),
        ("The Silence of the Lambs", "Thriller", "Jonathan Demme", 1991, 5),
        ("Dune : Partie 2", "Science-Fiction", "Denis Villeneuve", 2024, 4),
        ("Man on Fire", "Action", "Tony Scott", 2004, 3),
        ("Dead Poets Society", "Drame", "Peter Weir", 1989, 5),
        ("Batman & Robin", "Action", "Joel Schumacher", 1997, 2),
        ("Catwoman", "Action", "Pitof", 2004, 1),
        ("Battlefield Earth", "Science-Fiction", "Roger Christian", 2000, 1),
        ("Gigli", "Comédie", "Martin Brest", 2003, 1),
        ("The Love Guru", "Comédie", "Marco Schnabel", 2008, 2),
        ("Movie 43", "Comédie", "Various", 2013, 1),
        ("Birdemic: Shock and Terror", "Thriller", "James Nguyen", 2010, 1),
        ("The Happening", "Thriller", "Night Shyamalan", 2008, 2),
        ("The Room", "Drame", "Tommy Wiseau", 2003, 1),
        ("Troll 2", "Comédie", "Claudio Fragasso", 1990, 2),
        ("Manos: The Hands of Fate", "Thriller", "Harold P. Warren", 1966, 1)]
        
        for m in range(len(data)):
                ajoute_film(data[m][0], data[m][1], data[m][2], data[m][3], data[m][4])

Passons à l’explication du menu déroulant comportant les filtres.

On constate qu’il y a deux filtres : un par genre et un par note.

Cela est possible grâce au bout de code suivant :

boitefiltre = Frame(nouvelle_fenetre, bg="#1d1e2e")
boitefiltre.place(x=80, y=200, width=920, height=63)

m_genre= Menubutton(boitefiltre, text="Genre", relief="raised", borderwidth=2, width=75)
m_genre.grid(row=1, column=0, sticky="s")

m_note = Menubutton(boitefiltre, text="Note", relief="raised", borderwidth=2, width=75)
m_note.grid(row=1, column=1, sticky="s")

boitefiltre.grid_rowconfigure(0, weight=1)

m_filtre = Label(boitefiltre, text="Appliquer des filtres de recherche", font=("Consolas",20), bg="#1d1e2e", fg="white")
m_filtre.grid(row=0, column=0, sticky="s", columnspan=5)

menuDeroulant1 = Menu(m_genre)
menuDeroulant1.add_command(label='Action', command=lambda: filtrer_par_genre('Action'))
menuDeroulant1.add_command(label="Science-Fiction", command=lambda: filtrer_par_genre('Science fiction'))
menuDeroulant1.add_command(label="Biopic", command=lambda: filtrer_par_genre('Biopic'))
menuDeroulant1.add_command(label="Drame", command=lambda: filtrer_par_genre('Drame'))
menuDeroulant1.add_command(label="Thriller", command=lambda: filtrer_par_genre('Thriller'))
menuDeroulant1.add_command(label="Comédie", command=lambda: filtrer_par_genre('Comédie'))
m_genre.configure(menu=menuDeroulant1)

menuDeroulant2 = Menu(m_note)
menuDeroulant2.add_command(label='★☆☆☆☆', command=lambda: filtrer_par_note(1))
menuDeroulant2.add_command(label="★★☆☆☆", command=lambda: filtrer_par_note(2))
menuDeroulant2.add_command(label="★★★☆☆", command=lambda: filtrer_par_note(3))
menuDeroulant2.add_command(label="★★★★☆", command=lambda: filtrer_par_note(4))
menuDeroulant2.add_command(label="★★★★★", command=lambda: filtrer_par_note(5))

Ce bout de code créée une boîte de filtres comprenant deux menus déroulants (m_genre et m_note). Ces menus permettent de sélectionner des options de filtrage par genre de film et par note. La boîte de filtres contient également un label (m_filtre) indiquant à quoi elle sert, pour faciliter la compréhension de l’utilisateur.

Les menus déroulants sont associés à des commandes qui déclenchent des fonctions (filtrer_par_genre et filtrer_par_note) et en fonction des sélections de l’utilisateur, cela applique les filtres de recherche pour afficher les films voulues.

Modification de la base de donnée

Sur cette page nous pouvons retrouver les 5 boutons principaux :

  • Créer la table film
  • Supprimer la table film
  • Ajouter un film a la table
  • modifier la table film
  • supprimer un film

Fonction Création Table

def create_table():
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    curseur.execute('''CREATE TABLE IF NOT EXISTS Film (
                        id_film INTEGER PRIMARY KEY AUTOINCREMENT,
                        nom TEXT,
                        genre TEXT,
                        realisateur TEXT,
                        annee_pub INTEGER,
                        note REAL
                    )''')
     messagebox.showinfo("Creation Table", "La table a été crée.")
    
    connection.commit()
    curseur.close()
    connection.close()

Cette fonction create_table permet de créer une table dans une base de données SQLite nommée 'base_films.db‘. Elle établit une connexion avec la base de donnée 'base_films.db‘ , permettant d’accéder à la base de donnée et d’y effectuer des operations, ajouter, supprimer, mettre à jour… Ainsi la requête SQL permet de creer la table Film avec les attributs suivants : id_film, nom, genre, realisateur, annee_pub, note)

De plus un message est affiché lorsque la table est crée.

Fonction Supprimer Table

def supprimer_table():
    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()
    
    curseur.execute("DROP TABLE IF EXISTS Film")
    messagebox.showinfo("Suppression effectuee", "La table a été supprimée.")
   
    connection.commit()
    curseur.close()
    connection.close()

Cette fonction supprimer_table permet de supprimer la table crée précédemment.

Fonction ajouter un film

Lorsque l’on souhaite ajouter un film, une nouvelle page s’ouvre.

La fonction permettant d’ouvrir cette page :

def page_ajouter_film():
    global variable, variable1, variable2, variable3, variable4  
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Ajouter un film", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)


    variable = tk.StringVar()
    variable.set("Nom du film")
    entree = Entry(nouvelle_fenetre, textvariable=variable, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree.pack(padx=10, pady=10)


    variable1 = tk.StringVar()
    variable1.set("Genre du Film")
    entree1 = Entry(nouvelle_fenetre, textvariable=variable1, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree1.pack(padx=10, pady=10)

    
    variable2 = tk.StringVar()
    variable2.set("Realisateur")
    entree2 = Entry(nouvelle_fenetre, textvariable=variable2, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree2.pack(padx=10, pady=10)

 
    variable3 = tk.StringVar()
    variable3.set("Annee de publication")
    entree3 = Entry(nouvelle_fenetre, textvariable=variable3, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree3.pack(padx=10, pady=10)

     
    variable4 = tk.StringVar()
    variable4.set("Note")
    entree4 = Entry(nouvelle_fenetre, textvariable=variable4, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree4.pack(padx=10, pady=10)

   
    bouton_ajouter_film = Button(nouvelle_fenetre, text="Ajouter le film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=ajouter_film, width=20, height=2)
    bouton_ajouter_film.pack(pady=20)
    
    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Ainsi cette fonction permet d’avoir un rendu graphique explicite pour l’utilisateur. En effet cette page est composé de 5 encadrés remplie d’un champ permettant de faire savoir a l’utilisateur ou rentrer le titre du film… Le bouton ajouter film permet d’ajouter le film a la base de donnée

Afin que cette page puisse fonctionner correctement il est imperatif d’avoir une fonction executant le code SQL qui va avec.

Code permettant l’ajout du film :

def ajouter_film():
    
    global variable, variable1, variable2, variable3, variable4, data  
    nom_film = variable.get()
    genre = variable1.get()
    realisateur = variable2.get()
    annee_pub = variable3.get()
    note = variable4.get()

    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()

    curseur.execute('''INSERT INTO Film (nom, genre, realisateur, annee_pub, note)
                        VALUES (?, ?, ?, ?, ?)''', (nom_film, genre, realisateur, annee_pub, note))

    connection.commit()
    curseur.close()
    connection.close() 

    messagebox.showinfo("Film ajoute", "Le film a ete ajoute avec succes.")

Cette fonction ajouter un film dans la base de données SQLite et informe l’utilisateur que le film a été ajouté . En effet la ligne 13 exécute une commande SQL qui insere le film dans la base de données. Les “?” sont des paramètres qui sont remplacés par les valeurs des variables spécifiées.

Fonction modifier la table

Lorsque l’on souhaite modifier la table, une nouvelle page s’ouvre.

Fonction permettant d’ouvrir la page de modification:

def update():
    global variable, variable1, variable2, variable3, variable4 
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")
    nouvelle_fenetre.title("Modifier un film")

    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Modifier un film", font=("Consolas",40) ,bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)

    variable = tk.StringVar()
    variable.set("Nom du film")
    entree = Entry(nouvelle_fenetre, textvariable=variable, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree.pack(padx=10, pady=10)

    variable1 = tk.StringVar()
    variable1.set("Genre du Film")
    entree1 = Entry(nouvelle_fenetre, textvariable=variable1, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree1.pack(padx=10, pady=10)

    variable2 = tk.StringVar()
    variable2.set("Realisateur")
    entree2 = Entry(nouvelle_fenetre, textvariable=variable2, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree2.pack(padx=10, pady=10)

    variable3 = tk.StringVar()
    variable3.set("Annee de publication")
    entree3 = Entry(nouvelle_fenetre, textvariable=variable3, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree3.pack(padx=10, pady=10)

    variable4 = tk.StringVar()
    variable4.set("Note")
    entree4 = Entry(nouvelle_fenetre, textvariable=variable4, font=("Helvetica",15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree4.pack(padx=10, pady=10)

    bouton_update_film = Button(nouvelle_fenetre, text="Mettre a jour le film", font=("Consolas",13), bg="#1d1e2e", fg="white", command=update_film, width=20, height=2)
    bouton_update_film.pack(pady=20)

    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Ce code est semblable a celui de la creation de la page ajouter_film puisque on y retrouve les champs ou l’on doit inserer les informations necessaire.

Comme la page precedente afin que celle ci puisse fonctionner correctement il est imperatif d’avoir une fonction executant le code SQL qui va avec.

def update_film():
    nom_film = variable.get()
    genre = variable1.get()
    realisateur = variable2.get()
    annee_pub = variable3.get()
    note = variable4.get()

    connection = sqlite3.connect('base_films.db')
    curseur = connection.cursor()

    curseur.execute('''UPDATE Film SET genre=?, realisateur=?, annee_pub=?, note=? WHERE nom=?''', (genre, realisateur, annee_pub, note, nom_film))

    connection.commit()
    curseur.close()
    connection.close()

    messagebox.showinfo("Film modifié", "Le film a été modifié.")

Cette fonction met à jour les informations d’un film spécifique dans une base de données SQLite en utilisant le nom du film comme clé, et informe l’utilisateur que le film a été modifié.

Fonction Supprimer un film

Fonction permettant d’afficher cette page :

def page_supprimer_film():
    global variable_title
    nouvelle_fenetre = tk.Toplevel(screen)
    nouvelle_fenetre.geometry("1080x720")
    nouvelle_fenetre.config(bg="#0e101c")

    def retour():
        nouvelle_fenetre.destroy()

    titre = Label(nouvelle_fenetre, text="Supprimer un film", font=("Consolas", 40), bg="#0e101c", fg="#b3b3b3")
    titre.pack(pady=50)

    variable_title = tk.StringVar()
    variable_title.set("Titre du film à supprimer")
    entree_title = Entry(nouvelle_fenetre, textvariable=variable_title, font=("Helvetica", 15), bg="#1d1e2e", fg="#b3b3b3", width=30)
    entree_title.pack(padx=10, pady=10)

    bouton_supprimer_film = Button(nouvelle_fenetre, text="Supprimer le film", font=("Consolas", 13), bg="#1d1e2e", fg="white", command=supprimer_titre, width=20, height=2)
    bouton_supprimer_film.pack(pady=20)

    bouton_retour = Button(nouvelle_fenetre, text="Retour", font=("Consolas",13), bg="#1d1e2e", fg="white", command=retour, width=20, height=2)
    bouton_retour.place(x=850, y=650)

Fonction permettant de faire marcher la page :

def supprimer_titre():
    title = variable_title.get()

    connection = sqlite3.connect('base_films.db')
    cursor = connection.cursor()

    cursor.execute("SELECT id_film FROM Film WHERE nom=?", (title,))
    result = cursor.fetchone()

    if result:
        film_id = result[0]

        cursor.execute("DELETE FROM Film WHERE id_film=?", (film_id,))
        messagebox.showinfo("Film supprimé", "Le film a été supprimé avec succès.")
    else:
        messagebox.showwarning("Film introuvable", "Aucun film trouvé avec ce titre.")

    connection.commit()
    cursor.close()
    connection.close()

Cette fonction supprime un film spécifique de la base de données SQLite en utilisant le titre du film comme clé, et informe l’utilisateur que le film a été supprimé ou qu’aucun film n’a été trouvé avec le titre donné.

Bouton Retour

En effet, a la fin du projet nous avons constater que il manquait un bouton afin de revenir en arrière. Sans bouton l’utilisateur est obligé de fermer la fenêtre par la croix.

Ainsi la fonction suivante permet de fermer la page actuelle afin de revenir sur la page précédente.

def retour():
        nouvelle_fenetre.destroy()

Télécharger le projet

Vous trouverez ici le .zip du projet que vous puissiez le télécharger. PS : si vous relancer le script, il vaut mieux recréer la base de donnée à l’aide de DB Browser.

En espérant que vous trouverez un bon film.

Projets

Un Explorateur de fractales en Python

Plongez dans des mondes visuels éblouissants créés à partir de simples équations mathématiques, les fractales. Découvrez des fractales emblématiques telles que Julia et Mandelbrot, personnalisez votre expérience avec des palettes de couleurs. Embarquez pour un voyage captivant au cœur de l’art mathématique !

Les prémices du projet

Tout débute durant mon année de seconde. En math, j’ai utilisé le script Mandelbrot initialement présent dans la calculatrice Numworks. Je me suis amusé à analyser le script et grâce à ça et à mon père, je suis tombé dans les fractales. 

Ensuite, j’ai commencé la NSI en première et j’ai réalisé mon premier tutoriel sur ledit script. Et en fin d’année, j’ai modifié ce script afin de faire de plus belles couleurs.

Cette année, j’ai eu envie d’aller bien plus loin, l’idée vient du fait que pour  réaliser de belles images de fractale, j’utilise des explorateurs de fractales trouvables facilement sur internet pour trouver des coordonnées intéressantes. Mais l’expression le dit bien, on n’est jamais mieux servi que par soi-même, et j’ai donc choisi pour mon dernier projet de lycée de faire mon propre explorateur de fractale.

Le début du projet

Je suis parti du script Julia que j’avais fait durant mon projet de fin d’année de première, vous pourrez retrouver l’explication du script ici (lien vers le tuto Julia palette). Ainsi, nous partons de ce script : 

from kandinsky import*
def julia(N_iteration):
    palette = []
    xmax = 2
    xmin = -2
    ymax = 1.3875
    ymin = -1.387
    r = 255
    g = 255
    b = 255
    for j in range(0,128):
        b = 255 -2 * j
        palette.append([r,g,b])
    for j in range(128,256):
        r = 255 -2 * (j-128)
        g = 255 -2 * (j-128)
        palette.append([r,g,b])
    for x in range(320):
        for y in range(222):
            i = 0
            z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J)
            c = complex(0.36,0.36)
            while i < N_iteration and abs(z) < 2:
                i = i + 1
                z = z*z+c
            couleur = palette[int(255*i/N_iteration)]
            col = color(couleur[0],couleur[1],couleur[2])
            set_pixel(x,y,col)

La logique

I – optimisation

Pour faire mon explorateur, je veux générer une nouvelle fractale à chaque zoom, dézoome, déplacement, sur la droite, la gauche, le haut, le bas en modifiant les valeurs des coins de l’image. Mais si je suis cette logique, le script Julia pose déjà un problème. En effet, le script met des dizaines de secondes à fabriquer une seule image, vous conviendrez qu’un zoom en deux minutes, c’est un peu lent non ? Étrangement, un tutoriel a été écrit durant les vacances de Noël 2023 et bizarrement ce script présente exactement ce dont j’ai besoin ici.

Ainsi, j’ai un script qui me permet de générer des images en .png de fractale rapidement. Maintenant, j’ai remplacé la bibliothèque Kandinsky par Pillow et j’ai entièrement modifié les calculs de la fractale pour utiliser NumPy. Nous avons donc ce script :

from PIL import Image
import time
import tkinter as tk
from tkinter import PhotoImage
import keyboard
import numpy as np

global xmax, xmin, ymax, ymin, réso_x, réso_y
#valeurs de base
xmax = 2
xmin = -2
ymax = 1.3875
ymin = -1.387
réso_x = 30720 
réso_y = 21312

def julia_PIL(N_iteration = 100):
    palette = []
    r = 255
    g = 255
    b = 255
    for j in range(0, 128):
        b = 255 - 2 * j
        palette.append((r, g, b))
    for j in range(128, 256):
        r = 255 - 2 * (j - 128)
        g = 255 - 2 * (j - 128)
        palette.append((r, g, b))

    palette = np.array(palette, dtype=np.uint8)

    # Créer une grille de coordonnées complexes
    x = np.linspace(xmin, xmax, réso_x)
    y = np.linspace(ymin, ymax, réso_y)
    X, Y = np.meshgrid(x, y)
    Z = X + 1J * Y

    # Initialiser une matrice d'indices pour la palette
    indices_matrix = np.zeros((réso_y, réso_x), dtype=int)

    for i in range(N_iteration):
        print(i)
        # Mettre à jour les pixels
        mask = np.logical_and(i < N_iteration, np.abs(Z) < 2)
        indices_matrix[mask] = np.round(255 * i / N_iteration).astype(int)

        # Mettre à jour Z pour les pixels actifs
        Z[mask] = Z[mask] ** 2 + complex(0.36, 0.36)

    # Créer une image à partir de la matrice d'indices
    img_array = palette[indices_matrix]

    # Créer une image PIL à partir de l'array
    img = Image.fromarray(img_array, 'RGB')

    # Sauvegarder l'image
    img.save('fractales_images/test.png')

II – Affichage de l’image 

Pour afficher l’image, j’utilise la fonction Pillow mixé à Tkinter. Pillow génère une image en .png et l’enregistre dans un dossier et à chaque nouvelle génération la nouvelle image remplace l’ancienne. 

Tkinter lui génère une fenêtre de la même résolution que l’image générée par Pillow. Ensuite, il va chercher dans le dossier cette image faite par Pillow et l’affiche dans la fenêtre précédemment créée.

def fractal_builder(N_iteration = 100,nom_img='Explorer_image/Image.png'):
  """
  script de génération de la fractale
  """
    # Créer une image à partir de la matrice des indices
    img_array = palette[indices_matrix]

    # Créer une image à partir de l'array
    img = Image.fromarray(img_array, 'RGB')

    # Sauvegarder l'image
    img.save(nom_img)    

def afficher_image():
    img = PhotoImage(file=chemin_image)
    # stocke l'image dans la variable img
    canvas.create_image(0, 0, anchor=tk.NW, image=img)
    #affiche l'image

III – déplacement et zoom 

Afin de me déplacer et de zoomer, je vais utiliser la bibliothèque keyboard. Elle me permet de détecter quand j’appuie sur les touches de mon clavier. Je crée donc une fonction “explorer” qui s’occupe des déplacements et de leur logique. Je vais ainsi modifier les valeurs des coordonnées des coins de l’image que je prends de la fractale puis la générer avec ses nouvelles valeurs dans la fenêtre de Tkinter.

def explorer(action):
    global xmax, xmin, ymax, ymin,img,nbr_img    
    x = xmax - xmin
    y = ymax - ymin
    
    if keyboard.is_pressed('up'):# zoom avant
        xmax = xmax - (x/20)
        xmin = xmin - (x/20)*-1
        ymax = ymax - (y/20)
        ymin = ymin - (y/20)*-1
        fractal_builder()
        afficher_image()      
    elif keyboard.is_pressed('down'):# zoom arrière
        xmax = xmax + (x/20)
        xmin = xmin + (x/20)*-1
        ymax = ymax + (y/20)
        ymin = ymin + (y/20)*-1
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('z'): # déplacement haut
        ymax = ymax - (y/40)
        ymin = ymin - (y/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('s'): # déplacement bas
        ymax = ymax + (y/40)
        ymin = ymin + (y/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('q'): # déplacement gauche
        xmax = xmax - (x/40)
        xmin = xmin - (x/40)
        fractal_builder()
        afficher_image()
    elif keyboard.is_pressed('d'): # déplacement droite
        xmax = xmax + (x/40)
        xmin = xmin + (x/40)
        fractal_builder()
        afficher_image()

IV – Menu, accueil

Au début du projet, ma volonté était qu’à la fin, j’ai réussi à faire un explorateur de fractaleS. Pouvoir explorer des fractales comme Julia et Mandelbrot que je maîtrise déjà et peut-être même d’autres.

Mais pour faire ça il faut que l’utilisateur puisse choisir dans un menu quelle fractale veut-il explorer et faire un menu, quand j’ai commencé le projet été pour moi le plus dur à réaliser.

Durant les vacances de février 2024 il nous a été demandé de faire un gestionnaire de base de données, d’ailleurs voici ce que j’ai réalisé (lien vers le projet). Mais quel rapport avec des fractales ? Eh bien le fait que ce projet utilise Tkinter pour réaliser un menu, exactement ce que cherche à faire et surtout, c’est simple ! (même si honnêtement, on ne dirait pas).

Parallèlement à ce projet de base de données, j’ai donc piqué des bouts de mon code pour le mettre dans mon explorateur et en seulement quelques minutes, j’ai un menu ! Il ne sert à rien et il est moche (et il le restera, je suis pas designer).

Faire une fenêtre avec Tkinter est plutôt simple à comprendre, on crée des variables qui vont être chaque élément de la page, texte, menu déroulant, champs pour écrire, etc. Pour les construire, Tkinter nous offre des fonctions comme Label() pour les textes, Entry() pour les champs d’écritures entre autres. Il ne nous reste qu’à trouver sur internet les paramètres à mettre à l’intérieur (comme text = “le texte que l’on veut afficher” pour un texte).

titre_acceuil = tk.Label(cadre_acceuil, text="Bienvenue sur cet explorateur de fractales", font=("Consolas", 20), bg="#C2C2C2", fg="black")
soustitre_acceuil = tk.Label(cadre_acceuil, text="Choisissez les paramètres de votre fractale", font=("Consolas", 15), bg="#C2C2C2", fg="black")
titre_acceuil.pack(pady=10)
soustitre_acceuil.place(x=130,y=50)

#création de la liste déroulante de choix des palettes
desc_liste_deroul_palette = tk.Label(cadre_acceuil, text="Choisissez la palette de couleur que vous voulez utiliser", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_palette.place(x=15,y=100)
liste_choix = ["Blanc - Jaune - Noir","Blanc - Noir"] # élément de la liste déroulante
variable_palette = tk.StringVar() # élément initial
variable_palette.set("Blanc - Noir")
liste_deroulante_palette = ttk.Combobox(cadre_acceuil, textvariable=variable_palette, values=liste_choix)
liste_deroulante_palette.place(x=15,y=125)
select = liste_deroulante_palette.get() # sélection de l'élément choisi dans la liste

#création de la liste déroulante de choix de la fractale
desc_liste_deroul_fractale = tk.Label(cadre_acceuil, text="Choisissez la fractale que vous voulez voir", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_fractale.place(x=15,y=145)
liste_choix = ["Julia","Mandelbrot"] # élément de la liste déroulante
variable_fractal = tk.StringVar() # élément intiaux
variable_fractal.set("Julia")
liste_deroulante_fractal = ttk.Combobox(cadre_acceuil, textvariable=variable_fractal, values=liste_choix)
liste_deroulante_fractal.place(x=15,y=170)

# création du champ de texte pour choisir la valeur de c
desc1_chmp_val_julia = tk.Label(cadre_acceuil, text="Si vous avez choisi de générer une fractale de Julia :", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc2_chmp_val_julia = tk.Label(cadre_acceuil, text="Choisissez la valeur de la constante c (où laisser par défaut), sachant c est un complexe", font=("Consolas", 10), bg="#C2C2C2", fg="black")
desc1_chmp_val_julia.place(x=15,y=195)
desc2_chmp_val_julia.place(x=25,y=220)
variable_r = tk.StringVar()
variable_r.set("0.36")
chmp_str_c_r_julia = tk.Entry(cadre_acceuil, textvariable=variable_r, font=("Helvetica",12), bg="#ffffff", fg="black", width=5)
chmp_str_c_r_julia.place(x=40,y=245)
variable_i = tk.StringVar()
variable_i.set("0.36")
chmp_str_c_i_julia = tk.Entry(cadre_acceuil, textvariable=variable_i, font=("Helvetica",12), bg="#ffffff", fg="black", width=5)
chmp_str_c_i_julia.place(x=100,y=245)

#création de la liste déroulante de choix de la résolution
desc_liste_deroul_resolution = tk.Label(cadre_acceuil, text="Choisissez la résolution de l'explorateurr", font=("Consolas", 12), bg="#C2C2C2", fg="black")
desc_liste_deroul_resolution.place(x=15,y=270)
liste_choix = ["320x222","480x333","720x555"]
variable_resolution = tk.StringVar()
variable_resolution.set("320x222")
liste_deroulante_resolution = ttk.Combobox(cadre_acceuil, textvariable=variable_resolution, values=liste_choix)
liste_deroulante_resolution.place(x=15,y=295)

V – Menu, faire devenir utile l’accueil

Maintenant qu’on a un superbe menu, rendons le utile. Pour cela, je crée des fonctions pour sélectionner la palette, la résolution, la fractale etc que j’affecte à une grande fonction start que j’affecte elle au bouton qui lance l’explorateur. Les fonctions de sélection récupèrent ce qui est écrit dans les champs de textes, menu déroulant pour pouvoir donner c’est paramètre à la fonction mère, start().

start (), elle initialise toutes les variables utiles à l’explorateur, supprime la fenêtre du menu, crée la fenêtre de l’explorateur, initialise l’explorateur avec la première image et lance les dernières fonctions nécessaires comme explorer().

def selection_reso(): # fonction de sélection de la résolution utilisée
    global réso_x,réso_y
    if liste_deroulante_resolution.get() == "320x222":
        réso_x = 320
        réso_y = 222
    if liste_deroulante_resolution.get() == "480x333":
        réso_x = 480
        réso_y = 333
    if liste_deroulante_resolution.get() == "720x555":
        réso_x = 720
        réso_y = 555

def selection_palette(): # fonction de sélection de la palette utilisée
    global palette
    palette = []
    select = liste_deroulante_palette.get()
    if select == "Blanc - Jaune - Noir":
        r, g, b = 255, 255, 255
        for j in range(0, 128):
            b = 255 - 2 * j
            palette.append((r, g, b))
        for j in range(128, 256):
            r = 255 - 2 * (j - 128)
            g = 255 - 2 * (j - 128)
            palette.append((r, g, b))
    else :
        palette = [[i,i,i] for i in range(255)]

def start(): # fonction de lancement de l'exploration
    global img,chemin_image,canvas,choix_fract,compl_r,compl_i, réso_x, réso_y
    selection_palette()
    # sélèction de la fractale
    if liste_deroulante_fractal.get() == "Julia":
        choix_fract = 0
    if liste_deroulante_fractal.get() == "Mandelbrot":
        choix_fract = 1
    selection_reso()
    compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get() # sélection des valeurs de c
    
    fenetre.destroy() #supprime la fenêtre de l'accueil
    cadre_acceuil_explo = tk.Tk() # crée la fenètre de l'explorateur
    cadre_acceuil_explo.title("Affichage d'une Image")
    
    chemin_image = "Explorer_image/Image.png"
    # Charge l'image
    img = PhotoImage(file=chemin_image)
    # Crée un widget Canvas pour afficher l'image
    canvas = tk.Canvas(cadre_acceuil_explo, width=réso_x, height=réso_y)
    canvas.pack()
    # initialisation de la 1ère fractale
    fractal_builder()
    afficher_image()
    cadre_acceuil_explo.mainloop()

# création et placement du bouton de lancement de l'explorateur
bouton_start_explo = tk.Button(cadre_acceuil, text="Commencer l'exploration", font=("Consolas",15), bg="white", fg="black", command = start)
bouton_start_explo.place(x=230,y=495)

VI – Menu, prévisualisation

Actuellement, j’ai donc un menu qui me permet d’explorer 2 fractales différentes avec x palette différente et une infinité de fractales de Julia. Mais pendant que je joue avec mon script, je me rends compte que c’est lourd de devoir mettre les paramètres, lancer l’exploration, se rendre compte que ce n’est pas ce qu’on veut, donc on relance le script et ainsi de suite… Une idée me vient alors en tête : “pouvoir prévisualiser la fractale qu’on génère dans le menu, ce serait bien non ?” et voilà un nouvel objectif et pas des moindres, il me demande de mettre plusieurs variables en global pour y accéder, d’utiliser les fonctions de sélection, etc.

Pour réaliser cette fonction, on fait la même chose que la fonction start(). Mais cette fois, on ne supprime ni ne crée aucune fenêtre, on vient juste placer un canvas, l’image de la fractale que nous voulons prévisualiser au bon endroit avec les bons paramètres.

def previsu():
    global palette, choix_fract,compl_r,compl_i, réso_x, réso_y
    #sélèction des diffèrents paramètres
    réso_x, réso_y = 240,167
    selection_palette()
    if liste_deroulante_fractal.get() == "Julia":
        choix_fract = 0
    if liste_deroulante_fractal.get() == "Mandelbrot":
        choix_fract = 1
    compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get()
    
    fractal_builder() # génération d'une fractale avec ces paramètres
    prévi = PhotoImage(file="Explorer_image/Image.png") # récupération de l'image
    # création du cardre et placement de l'image dans le cadre
    label_image = tk.Label(cadre_acceuil, image=prévi)
    label_image.place(x=245,y=310)
    label_image.mainloop()

VII – Menu, tutoriel

La dernière barrière entre mon explorateur et l’utilisateur est ainsi ma logique. En effet, les touches que j’ai choisies pour mon explorateur ne sont sûrement pas les meilleures, donc je rajoute une nouvelle page à mon menu pour y écrire un petit guide des touches. Je profite de ça pour ajouter deux nouvelles fonctions. La première fonction récupère les coordonnées de la fractale qu’on voit pour les nerds. La seconde, plus complexe, prend des “screenshot” de ce qu’on voit dans l’explorateur. En réalité, elle génère une fractale avec les mêmes paramètres que celle que l’on voit dans l’explorateur, mais change son nom au moment de l’enregistrer.

#création du cadre de tutoriel dans la fenêtre de l'accueil
cadre_acceuil = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720)
cadre_acceuil.pack_propagate(False) 
cadre_tuto = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720)
cadre_tuto.pack_propagate(False) 
# création et placement du texte du tuto
titre_tuto = tk.Label(cadre_tuto, text="Guide des Touches", font=("Consolas", 20), bg="#C2C2C2", fg="black")
soustitre_tuto = tk.Label(cadre_tuto, text="Vous vous sentez un peu  perdu ?", font=("Consolas", 15), bg="#C2C2C2", fg="black")
titre_tuto.pack()
soustitre_tuto.pack(pady=10)
texte_tuto = tk.Label(cadre_tuto, text="z - aller vers le haut \ns - aller vers le bas \nq - aller à droite \nd - aller à gauche \n\nflèche du haut - zoom avant \nflèche du bas - zoom arrière \n\nm - renvoie les coordonnées de \n    où vous êtes dans la fractale \n\nc - sauvegarde un PNG de ce que \n    vous voyez dans le dossier Explorer_image \n", font=("Consolas", 13), bg="#C2C2C2", fg="black")
texte_tuto.pack()
fin = tk.Label(cadre_tuto, text="Amusez-vous !", font=("Consolas", 15), bg="#C2C2C2", fg="black")
fin.pack(pady=20)

#nouvelles fonctions ajoutées à l'explorateur
def explorer(action):
    global xmax, xmin, ymax, ymin,img,nbr_img
    x = xmax - xmin
    y = ymax - ymin
    """
    différentes fonctions de déplacements
    """
    elif keyboard.is_pressed('m'): # récupération de nos coordonnées (pour les nerds)
        print("xmax = ",xmax)
        print("xmin = ",xmin)
        print("ymax = ",ymax)
        print("ymin = ",ymin)
    elif keyboard.is_pressed('c'): # prend un screenshot de la fractale
        print("Image enregistrée !")
        nbr_img +=1
        fractal_builder(100,'Explorer_image/Image_saved'+str(nbr_img)+'.png')
        afficher_image()
        print(nbr_img)

Conclusion

En conclusion, ce projet ma permis d’utiliser la plupart de mes compétences. Il m’a suivi durant la moitié de mon année de terminale.

Galerie

Quelques exemples de fractale que vous pourrez à votre tour explorer :

Script complet

Voici le script final en .7z, il vous suffit de l’extraire et vous pourrez directement commencer à explorer !

vous pouvez également voir les futures mise à jour sur GitHub ici.

Projets

Un site pour mettre à l’épreuve vos connaissances du…

web.snt.nsi.xyz est un projet répertoriant 10 énigmes sur-mesure accessibles pour un élève en classe de seconde. Plus globalement, ce site va vous permettre de découvrir différents aspects de l’informatique et du Web, en passant par l’histoire de l’informatique jusqu’au fonctionnement d’un site Web. Aujourd’hui vous est présenté la V2 de ce projet, une mise à jour majeure.

La V1 de ce projet

Ce projet, dopé au HTML, CSS, JavaScript, et PHP, a été lancé par Ilyas R. qui a conçu et développé l’entièreté du site, incluant les 10 énigmes, le système de chronométrage, etc. Il a également échafaudé la direction artistique en s’aidant d’une librairie CSS (également appelé «Framework CSS»). Il s’agit de PureCSS, une librairie gratuite et open-source que vous pouvez également utiliser dans vos projets. De manière générale, Ilyas a conçu la structure principale du site. Le site web.snt.nsi.xyz est né suite aux vacances de la Toussaint.

Une V2 ?

Pour les vacances d’hiver, une V2 du site a été envisagée incluant un bon nombre de fonctionnalités. Pour ce faire, Thomas S. a rejoint le projet et l’objectif principal est maintenant que le site fonctionne autour d’une base de données. Voici le cahier des charges que nous avons rédigés :

  • Système de gestion de session :
    • Créer une session (en tant que professeur notamment)
    • Rejoindre une session (en tant qu’élève notamment)
    • Observation d’une session (pour que le professeur voit quelles énigmes ont été faites)
    • Statistiques pour les sessions (pourcentages des énigmes résolues, etc.)
    • Et bien plus…
  • Système de comptes (Créer un compte / Se connecter)
  • Système pour parcourir des sessions précédemment créées
  • Système de traductions
  • Ainsi que d’autres petites fonctionnalités…

À partir de là, l’aventure et les galères vont commencer.

Structure de la base de données

Tout d’abord, avant de nous lancer directement à l’aveugle dans le développement, nous avons conçu et organisé notre base de données, en déterminant toutes les relations à créer ainsi que les attributs. Ainsi, voici la structure finale de notre base de données relationnelle.

Nous n’aurons, à notre plus grand regret, pas le temps nécessaire de développer un système de permissions élaboré et un système de gestion des traductions, ainsi dans la V2, les relations groups, permissions, et assets ne sont pas présentes.

Des briques fondamentales

Avant toute chose, il est important de concevoir des fonctions et des bouts de pages qui seront utilisés à maintes reprises. Par exemple le panneau de navigation à gauche.
Ou encore, ce qui nous intéresse le plus ici des fonctions pour manipuler la base de données. Il a fallu créer des fonctions pour créer, lire, modifier et supprimer (CRUD) des enregistrements dans des relations (tables) de notre base de données.

Créer un enregistrement

Voici la fonction que nous avons développé pour créer un enregistrement dans notre base de données :

function addRow($database, $relation, $values) {
    $attributs = getAttributs($database, $relation, 0); // Permet de récupérer la liste des attributs de la relation, hormis la clé primaire
    $attributs_query = "(".implode(", ", array_map(function($value) {
        return $value;
    }, $attributs)).")";
    $values_query = "(".implode(", ", array_map(function($value) {
        return "\"".$value."\"";
    }, $values)).")";
    $sql = "INSERT INTO $relation $attributs_query VALUES $values_query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 3 paramètres, $database, $relation, et $values, le premier indique dans quelle base de données agir (en l’occurrence, ce sera toujours la même étant donné que nous possédons uniquement une base de données), le deuxième indique la relation (table) où agir, et le dernier les valeurs à ajouter dans l’enregistrement. La fonction utilise ainsi la requête SQL INSERT INTO $relation {attributs} VALUES $values, en déterminant automatiquement les attributs de la relation où agir, il n’est pas nécessaire de rentrer en paramètre les attributs.

Lire des enregistrements

Voici la fonction que nous avons développé pour lire un ou plusieurs enregistrements dans notre base de données :

function getRows($database, $relation, $attribut, $query, $force_multiples_rows = 0) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return rowsCount($database, $relation, $query) > 1 || $force_multiples_rows == 1 ? $stmp->fetchAll() : $stmp->fetch(PDO::FETCH_ASSOC);
}

La fonction prend 4 paramètres, $database, $relation, $attribut, et $query. De la même manière que la fonction addRow(), nous retrouvons les mêmes paramètres $database et $relation, mais ici avec 2 autres paramètres, $attribut qui représente le ou les attributs dans le ou lesquels les données doivent être collectées, ainsi que $query qui représente la ou les conditions à appliquer pour filtrer les lignes de la table de la base de données. La fonction utilise ainsi la requête SQL SELECT $attribut FROM $relation WHERE $query. Sachant que si nous ne voulons pas mettre de condition, il suffit de renseigner « 1 » dans le paramètre $query.

Mettre à jour un enregistrement

Voici la fonction que nous avons développé pour mettre à jour un enregistrement dans notre base de données :

function updateRow($database, $relation, $attributs_values, $query){
    $setCommand = "";
    $attributs = array_keys($attributs_values);
    for ($i = 0; $i < count($attributs) - 1; $i++){
        $setCommand = $setCommand.$attributs[$i]." = \"".$attributs_values[$attributs[$i]]."\", ";
    }
    $setCommand = $setCommand.$attributs[count($attributs) - 1]." = \"".$attributs_values[$attributs[count($attributs) - 1]]."\"";
    $sql = "UPDATE $relation SET $setCommand WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 4 paramètres, $database, $relation, $attributs_values, et $query. De la même manière que la fonction addRow() et getRows(), nous retrouvons les mêmes paramètres $database, $relation et $query, mais ici avec un autre paramètre, $attributs_values qui représente, pour simplifier au plus, un tableau de la forme array(key => value) avec l’attribut en tant que key et la nouvelle valeur en tant que value. La fonction utilise ainsi la requête SQL UPDATE $relation SET {modifications} WHERE $query.

Supprimer un enregistrement

Voici la fonction que nous avons développé pour supprimer un enregistrement dans notre base de données :

function delRow($database, $relation, $query) {
    $sql = "DELETE FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
}

La fonction prend 3 paramètres, $database, $relation, et $query. De la même manière que la fonction addRow() et getRows(), et updateRows() nous retrouvons les mêmes paramètres $database, $relation et $query. La fonction utilise ainsi la requête SQL DELETE FROM $relation WHERE $query.

D’autres fonctions agissant sur la base de données

On remarque finalement une structure souvent assez similaire, plus ou moins complexe. Au niveau des paramètres pour la fonction, on retrouve toujours $database, une variable qui indique donc sur quelle base de données on agit. Les autres paramètres demandés sont en rapport avec la requête SQL que l’on souhaite exécuter. Que ce soit la création, la lecture, la modification ou la suppression, on a toujours besoin d’indiquer la $relation qui est donc une variable indiquant dans quelle relation (table) on va faire cette requête. Suite à cela, les autres variables dépendent de la requête.

D’autres fonctions peuvent être pratiques, comme une fonction pour récupérer le nombre d’enregistrements pour une table ($relation) selon une condition de recherche ($query) :

function rowsCount($database, $relation, $query) {
    $sql = "SELECT * FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return $stmp->rowCount();
}

Par exemple, je veux le nombre de sessions actuellement actives :

$db = new PDO("mysql:host=localhost:3306;dbname=web_snt_nsi_xyz", "root", "root");
$nbr_sessions_actives = rowsCount($db, "sessions", "status = 1");

Il y a bien plus de fonctions que nous avons codées en avance pour pouvoir les réutiliser plus tard, comme createSession() qui va crée un session dans la base de donnée, getRowsInJson(), qui fait la même chose que getRows() mais qui renvoie le résultat sous un autre format (le format JSON). De même on retrouve une fonction qui crée un utilisateur, qui le modifie, qui le supprime, en bref on s’est fait notre boite à outils de fonctions.

/**
* Cette fonction crée un enregistrement dans la table sessions avec des informations sur la sessions :
* son code, le propriétaire de la session, la date de création, son statut, etc. 
*/

function createSession($database, $id_owner) {
    $codeSession = generateSessionCode($database);
    addRow($database, "sessions", array($codeSession, $id_owner, date('Y-m-d H:i:s', time()), 1));
    $id_session_created = getRows($database, "sessions", "*", "id_owner = $id_owner AND status = 1")["id"];
    updateLocalDB("[]", "../js/db-$id_session_created.json");
}

/**
* Il est intéressant de comparer cette fonction avec getRows() pour voir les différences qu'il y a entre les deux.
*/

function getRowsInJSON($database, $relation, $attribut, $query) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return json_encode($stmp->fetchAll());
}

function getRows($database, $relation, $attribut, $query, $force_multiples_rows = 0) {
    $sql = "SELECT $attribut FROM $relation WHERE $query";
    $stmp = $database->prepare($sql);
    $stmp->execute();
    return rowsCount($database, $relation, $query) > 1 || $force_multiples_rows == 1 ? $stmp->fetchAll() : $stmp->fetch(PDO::FETCH_ASSOC);
}

Gestion des comptes

Ce système est pour les utilisateurs possédants un compte vérifié sur le site, c’est-à-dire ceux qui peuvent créer une session, ou ceux qui peuvent administrer le site.

Pour cela, comme dit plus haut, des fonctions ont été conçues pour créer, modifier et supprimer des utilisateurs. Ces fonctions manipulent directement la base de données, et passent donc par les fonctions plus basiques addRow(), updateRow(), getRows(), ou delRow(). Maintenant pour permettre ces actions, il est important de faire une interface graphique. Pour cela, une page est conçue pour pouvoir créer un utilisateur, pour avoir la liste des utilisateurs et pouvoir les gérer. Voici à quoi elle ressemble :

Suite à cela, il est important qu’il y ait un système de permissions pour pouvoir empêcher certains utilisateurs d’accéder à certaines pages. En effet, on veut que seul les administrateurs aient accès à cette page, et même à d’autres.

Le système de session

Lorsqu’un utilisateur n’a pas de session active, on lui propose de créer une session, à terme, il pourra configurer certains éléments de la session (notamment la durée).

Le système de session inclut un système de génération de codes, qui permettra à d’autres utilisateurs de rejoindre la session liée à ce code.

Une session va permettre à son propriétaire d’administrer les utilisateurs temporaires (les élèves) et d’observer leur avancement sur les énigmes.

De plus, pour les administrateurs, il sera possible d’avoir des statistiques combinées de toutes les sessions qui ont été créées. De même, il leur sera possible de parcourir les sessions en cours et les anciennes.

Une interface pour rejoindre une session ou se connecter

Voici l’interface de la page pour s’identifier sur le site :

On peut rejoindre une session, se connecter, créer un compte, ou accéder aux énigmes avec le menu à gauche.

Conclusion

Finalement, ce projet a été très enrichissant pour nous, il nous a vraiment permis de nous améliorer en PHP, nous avons énormément travaillé dessus, et nous sommes fiers du résultat, malgré le fait qu’on ait pas eu le temps d’intégrer tout ce qui était prévu à la base (rendez-vous à la V3 🤫).

La V2 est accessible à l’adresse : web.snt.nsi.xyz/dev/

Code source du projet

Vous pouvez retrouver le code source du projet dans notre dépôt GitHub.

Projets

PhpMonDésastre


Dans le cadre du projet SGBDR, j’ai eu l’occasion de créer un site Internet en PHP. Je vous propose donc de découvrir mon projet, nommé phpMonDésastre. Il s’agit d’une version simplifiée de phpMyAdmin qui vous permet d’interagir de manière simplifiée avec une base de données.

Idée

Mon idée était de créer une version simplifiée de phpMyAdmin où les utilisateurs pourraient venir interagir de manière minimaliste avec une base de données et observer les résultats. Cette version, que je nommerais phpMonDésastre, devrait suivre les règles du CRUD, c’est-à-dire Créer, Lire, Mettre à jour et Supprimer.

Cahier des charges

Pour accéder à mon site, les utilisateurs doivent d’abord se connecter à leur session. S’ils n’ont pas de compte, ils devront en créer un. Une fois connectés, ils pourront parcourir les différentes tables de la base de données ainsi que leur contenu. Ensuite, ils auront la possibilité d’exécuter uniquement quatre types de commandes respectant la règle CRUD : créer une table, ajouter du contenu à une table, modifier le contenu d’une table et supprimer une table. Cela permettra de limiter les possibilités des utilisateurs afin d’éviter les abus de la base de données mise à leur disposition.

Mise en place


Pour mettre en place ce projet, il faut commencer par mettre en place une base de données qui contiendra les informations des comptes utilisateurs et qui recevra ces expériences des utilisateurs.

<?php
/*On se connecte à la base de données en entrant les informations nécessaires,
telles que le nom de l'hôte, le nom de la base, les identifiants et le mot de passe.
Ensuite, on définit cette connexion sur la variable $mysqlClient que l'on utilisera
pour gérer toutes les requêtes SQL. 
On permet aussi l'affichage des erreurs pour pouvoir déboguer plus facilement*/
$sql_host="";
$sql_id="";
$sql_password="";
$sql_dbname="";

try {
    $mysqlClient = new PDO(sprintf('mysql:host=%s;dbname=%s;charset=utf8', $sql_host, $sql_dbname),
    $sql_id,
    $sql_password);

    $mysqlClient->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (Exception $exception) {
    die('Erreur : ' . $exception->getMessage());
}



Ensuite, on met en place la page de connexion. D’abord, on s’occupe de la partie frontend, puis de la partie backend.

<!DOCTYPE html>
<html lang="fr" >
<head>
    <meta charset="UTF-8">
    <title>Login Form</title>
    <link rel="stylesheet" href="style.css">
</head>
 <body>
    <?php if (!isset($_SESSION['LOGGED_USER'])) : ?> // On vérifie si l'utilisateur est connecté.
        <center><div class = "message">
            <?php if (isset($_SESSION['LOGIN_ERROR_MESSAGE'])) : ?> // Si une erreur survient, on l'affiche.
                <?php echo $_SESSION['LOGIN_ERROR_MESSAGE'];
                unset($_SESSION['LOGIN_ERROR_MESSAGE']); ?> // Une fois affichée, on l'enlève.
            <?php endif; ?>
            <?php if (isset($_SESSION['SIGNUP_ERROR_MESSAGE'])) : ?> // de même avec les erruer pour l'incription
                <?php echo $_SESSION['SIGNUP_ERROR_MESSAGE'];
                unset($_SESSION['SIGNUP_ERROR_MESSAGE']); ?>
            <?php endif; ?>
        </div></center>
        <section class = "fond-noir"> 
            <div class="signin"> 
                <div class="content"> 
                    <h2>Sign In</h2> 
                        <form class = 'form' action="submit_login.php" method="POST"> 
                            <div class="inputBox" action="submit_login.php" method="POST"> // Boîte pour demander l'adresse e-mail.
                                <input type="Email" name="email" required> <i>Username</i> 
                            </div> 
                            <div class="inputBox" action="submit_login.php" method="POST"> // Boîte pour demander le mot de passe.
                                <input type="password" name="password" required> <i>Password</i> 
                            </div> 
                            <div class="links"> 
                                <a href="forgot_psw.php">Forgot Password</a> <a href="sing_up.php">Signup</a> // Bouton renvoyant vers la page de réinitialisation de mot de passe et la page d'inscription.
                            </div> 
                            <div class="inputBox"> 
                                <input type="submit" value="Login"> // Envoyer la requête de connexion.
                            </div>
                        </form>
                    </div> 
                </div> 
            </div> 
        </section>
    <?php endif; ?>
 </body>
</body>
</html>
<?php
// On démarre la session.
session_start();
/* On importe les différentes pages PHP nécessaires pour l'utilisation complète du script,
telles que "databaseconnect.php" mentionnée ci-dessus pour se connecter à la base de données.*/
require_once(__DIR__ . '/config/mysql.php');
require_once(__DIR__ . '/databaseconnect.php');
require_once(__DIR__ . '/variables.php');
require_once(__DIR__ . '/functions.php');

$postData = $_POST;

// On vérifie que les informations demandées soient bien rentrées.
if (isset($postData['email']) && isset($postData['password'])) { 
  	// On vérifie la validité de l'email fourni.
    if (!filter_var($postData['email'], FILTER_VALIDATE_EMAIL)) {
        $_SESSION['LOGIN_ERROR_MESSAGE'] = 'Il faut un email valide pour soumettre le formulaire.';
    } else {
      	// On vérifie la présence des informations données dans la base de données.
        foreach ($users as $user) {
            if (
                $user['email'] === $postData['email'] &&
                $user['password'] === $postData['password']
            ) {
                $_SESSION['LOGGED_USER'] = [
                    'email' => $user['email'],
                    'user_id' => $user['user_id'],
                ];
            }
        }
		// Si elles ne sont pas présentes dans la base de données, on renvoie une erreur.
        if (!isset($_SESSION['LOGGED_USER'])) {
            $_SESSION['LOGIN_ERROR_MESSAGE'] = sprintf(
                'Les informations envoyées ne permettent pas de vous identifier : (%s/%s)',
                $postData['email'],
                strip_tags($postData['password'])
            );
        }
      // De même pour la page d'inscription.
        if (isset($_SESSION['SIGNUP_ERROR_MESSAGE'])) {
            $_SESSION['SIGNUP_ERROR_MESSAGE'] = sprintf(
                'Les informations envoyées ne permettent pas de vous inscrire'
            );
        }
    }

    redirectToUrl('index.php');
}


En ce qui concerne la page d’inscription, c’est à peu près le même principe, sauf qu’au lieu de vérifier la présence des informations entrées dans la base de données, on les inscrit dans celle-ci avant de rediriger l’utilisateur vers la page de connexion afin qu’il puisse se connecter avec son profil fraîchement créé.


Une fois cette étape franchie, vous arrivez sur une page de connexion semblable à celle-ci :


Une fois connecté, on arrive sur la page principale.

Pour concevoir la page principale, j’ai élaboré une maquette que je suivrai pour la création de la page :

Tout d’abord, nous procédons à l’affichage des tables :

<form class="form" action="index.php" method="post"> // Affichage des tables
    <?php foreach ($TABLES as $table) : ?>
         <?php if ($table[0] === 'users') : ?>
         <?php else : ?>
             <input type="checkbox" value="<?= $table[0] ?>" name="choix_cle" onchange="disableOtherCheckboxesTable(this)" <?= isset($_SESSION['choix_cle']) && $_SESSION['choix_cle'] === $table[0] ? 'checked' : '' ?>>
             <label for="<?= $table[0] ?>"> <?= $table[0] ?></label><br>
         <?php endif ?>
    <?php endforeach; ?>
    <div calss = "case-boutton">
         <input class="submit-bouton" type="submit" value="Valider">
    </div>
</form>

L’affichage des colonnes :

<form class="form" action="index.php" method="post">
     <?php foreach ($CLE as $clecle) : ?>
          <input type="checkbox" name="choix_contenu" value="<?= $clecle['Field'] ?>" <?= isset($_POST['choix_contenu']) && $_POST['choix_contenu'] === $clecle['Field'] ? 'checked' : '' ?>>
          <label for="<?= $clecle['Field'] ?>"> <?= $clecle['Field'] ?></label><br>
     <?php endforeach; ?>
     <div calss = "case-boutton">
          <input class="submit-bouton" type="submit" value="Valider">
     </div>
</form>

L’affichage du contenu :

<form class="form" action="index.php" method="post">
     <?php foreach ($CONT as $cont1) : ?>
          <?php foreach($cont1 as $cont2) : ?>
               <input type="checkbox" name="choix_contenu" value="<?= $cont2 ?>" <?= isset($_POST['choix_contenu']) && $_POST['choix_contenu'] === $cont2 ? 'checked' : '' ?>>
               <label for="<?= $cont2 ?>"> <?= $cont2 ?></label><br>
          <?php endforeach; ?>
     <?php endforeach; ?>
</form>

Ensuite, on procède à l’affichage des commandes :

<div class="div-submit-bouton">
     <form class="form" action="index.php" method="post">
          <center>
          CREATE TABLE IF NOT EXISTS <input class="inputBox2" type="text" name="cree_table" required> (<br><input class="inputBox1" type="text" name="cree_cont" required>);
          <input class="submit-bouton2" type="submit" value="Crée une table"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          INSERT INTO <input class="inputBox2" type="text" name="ajout_table" required> (<input class="inputBox2" type="text" name="ajout_valeurs1" required>) VALUES (<input class="inputBox2" type="text" name="ajout_valeurs2" required>);
          <input class="submit-bouton2" type="submit" value="Ajouté"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          UPDATE <input class="inputBox2" type="text" name="modif_table" required> SET <input class="inputBox2" type="text" name="modif_def" required> WHERE <input class="inputBox2" type="text" name="modif_condi" required>;
          <input class="submit-bouton2" type="submit" value="Modifier"/>
          </center>
     </form>
     <form class="form" action="index.php" method="post">
          <center>
          DROP TABLE <input class="inputBox2" type="text" name="drop_table" required>;
          <input class="submit-bouton2" type="submit" value="Supprimé"/>
          </center>
     </form>
</div>

Et voici le résultat :

Conclusion

En conclusion, phpMonDésastre représente une tentative de créer une version simplifiée de phpMyAdmin, offrant une interaction minimale avec une base de données. Bien que le projet suive les principes du CRUD et offre des fonctionnalités de base telles que la création, la lecture, la mise à jour et la suppression de données, il reste encore des améliorations à apporter. L’intégration de fonctionnalités supplémentaires et l’amélioration de l’interface utilisateur pourraient rendre cette plateforme plus conviviale et fonctionnelle. En fin de compte, phpMonDésastre constitue un premier pas vers un outil de gestion de base de données simplifié, mais il reste encore du travail à faire pour atteindre son plein potentiel.

Dans le lien de téléchargement ci-dessous, vous trouverez le projet dans son intégralité. Si vous utilisez un environnement local, vous devrez utiliser XAMPP pour exécuter PHP. Si vous hébergez le projet sur un serveur, vous devrez modifier la configuration de « databaseconnect.php » comme présenté précédemment.

Projets

SPA – les bases de données

En 2022, on estime que 41 186 animaux ont été adoptés à la SPA et 44 199 animaux ont été recueillis par celle-ci. Afin de pouvoir répertorier les animaux présents à la SPA, une table de données est l’outil indispensable.

I – Présentation du projet

Pour commencer, la réalisation de la base de données se porte sur la simulation d’une SPA. Celle-ci se compose d’une page d’Acceuil comportant le menu en haut de la page.

Puis d’une page Ajouter où l’on rentre les informations sur un nouvel animal.

Par la suite, d’une page Modifier permettant de modifier les informations sur un animal déjà enregistré sur la base de données en rentrant son id et en choisissant la donnée à modifier. De plus, lorsque vous choisissez de supprimer un animal, il vous suffit de rentrer son id et de cliquer sur Supprimer l’animal ayant cet id.

Et enfin, la page Parcourir où l’on choisit par quel type de donnée nous souhaitons faire la recherche (id, espèce, race, nom, date et lieu) et où l’on écrit la valeur recherchée ou si c’est une date, si elle est égale, plus grande, plus petites, etc.

Pour chaque page, il est possible de supprimer toute la table, c’est-à-dire supprimer tout le tableau crée. Lorsque vous ajoutez et modifiez, le résultat de Parcourir se trouve sur la fenêtre blanche en dessous de Rechercher.

II – Mise en place du projet

Dans un premier temps, il a été primordial d’importer les librairies présentes pour la création de la base de données.

from tkinter import *
from tkinter import ttk
from tkinter.messagebox import showinfo
import sqlite3

La première ligne de code permet d’utiliser le nom des classes et fonctions définies sur tkinter. La seconde permet de créer des widgets. La troisième, showinfo permet de fournir des instructions à l’utilisateur. Enfin, d’importer le module SQLite3 permet d’interagir avec la base de données en .db .

Après avoir importé les modules dont on a besoin, il faut établir une connexion avec la base de données « base_SPA.db » et créer un curseur pour exécuter les commandes SQL .

connection = sqlite3.connect('base_SPA.db')
curseur = connection.cursor()
Ensuite, il faut créer les différentes fenêtres telles que accueil, ajouter, modifier, parcourir qui sont présents tout le long et dès le début à la page Accueil avec le bouton pour vider la table, c'est-à-dire effacer les données de la table.
from tkinter import *
from tkinter import ttk
from tkinter.messagebox import showinfo
import sqlite3

#génération du lien de la base de donnée
connection = sqlite3.connect('base_SPA.db')
curseur = connection.cursor()

#génération de la base de donnée si elle n'existe pas
curseur.execute("CREATE TABLE IF NOT EXISTS ANNIMAL(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, nom TEXT, espèce TEXT, race TEXT, age INT, trouvé_où TEXT)")

connection.commit()
curseur.close()
connection.close()

# création de toutes les fenêtres
def afficher_nouvelle_fenetre(message):
    fenetre.withdraw()  # Masque la fenêtre actuelle
    nouvelle_fenetre = Toplevel(fenetre)
    nouvelle_fenetre.title("Nouvelle Fenêtre")
    nouvelle_fenetre.geometry("720x555") 
    Label(nouvelle_fenetre, text=message).pack(padx=20, pady=20)

def affiche_acceuil():
    cadre_pile.append(cadre_acceuil)  # création de la page d'accueil
    afficher_cadre(cadre_acceuil)

def affiche_ajoute():
    cadre_pile.append(cadre_ajoute)  # création de la page ajoute
    afficher_cadre(cadre_ajoute)

def affiche_modifier():
    cadre_pile.append(cadre_modifier)  # création de la page modifier
    afficher_cadre(cadre_modifier)
    
def affiche_parcourir():
    cadre_pile.append(cadre_parcourir)  # création de la page parcourir
    afficher_cadre(cadre_parcourir)


def afficher_cadre(cadre):
    for c in [cadre_acceuil, cadre_ajoute, cadre_modifier, cadre_parcourir]:
        c.pack_forget()
    cadre.pack()

#génération de la fenêtre
fenetre = Tk()

fenetre.title("SPA - les bases de données")
fenetre.geometry("720x555")
fenetre.config(bg="#C2C2C2")
fenetre.iconbitmap("logo.ico")

#fonction pour vider la table
def vider_table():
    connection = sqlite3.connect('base_SPA.db')
    curseur = connection.cursor()
    curseur.execute("DROP TABLE ANNIMAL")
    curseur.execute("CREATE TABLE ANNIMAL(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, nom TEXT, espèce TEXT, race TEXT, age INT, trouvé_où TEXT)")
    connection.commit()
    curseur.close()
    connection.close()
    
bouton_vider_table = Button(fenetre, text="vider la table", font=("Consolas",15), bg="white", fg="black",command = vider_table)
bouton_vider_table.place(x=530,y=480)

# Cadres pour différentes pages
cadre_acceuil = Frame(fenetre, bg="#C2C2C2")
cadre_ajoute = Frame(fenetre, bg="#C2C2C2")
cadre_modifier = Frame(fenetre, bg="#C2C2C2")
cadre_parcourir = Frame(fenetre, bg="#C2C2C2")

cadre_acceuil.pack()

# Création du menu
menubar = Menu(fenetre)

# Ajout des commandes directement au menu principal
menubar.add_command(label="Acceuil", command=affiche_acceuil)
menubar.add_command(label="Ajouter", command=affiche_ajoute)
menubar.add_command(label="Modifier", command=affiche_modifier)
menubar.add_command(label="Parcourir", command=affiche_parcourir)

# Configuration de la fenêtre avec la barre de menus
fenetre.config(menu=menubar)

# Initialisation de la pile des cadres
cadre_pile = [cadre_acceuil]

fenetre.mainloop()

Les fonctions permettent de gérer la navigation entre les différentes pages en fonction des actions de l’utilisateur. De plus, elles permettent de masquer les cadres précédemment affichés et affichent le cadre correspondant à la page demandée.

Après avoir créé nos différentes pages, il faut les construire, rajouter du texte, du champ de texte, etc. Tout d’abord, commence la page d’accueil, très simplement, on crée des blocs :

# création des "blocs" 
titre_acceuil = Label(cadre_acceuil, text="Bienvenue sur la base de données de la SPA", font=("Consolas", 20), bg="#C2C2C2", fg="black")
soustitre_acceuil = Label(cadre_acceuil, text="DONNONS-LEUR AUTANT QU'ILS NOUS APPORTENT !", font=("Consolas", 13), bg="#C2C2C2", fg="black")

# affichage de ces blocs
titre_acceuil.pack(pady=20)
soustitre_acceuil.pack()

Après cela, il ne nous reste plus qu’à créer les autres pages.

# création des different blocs + affichage des blocs
titre_ajoute = Label(cadre_ajoute, text="AJOUTER", font=("Consolas", 20), bg="#C2C2C2", fg="black")
titre_ajoute.pack(pady=20)
soustitre_ajoute = Label(cadre_ajoute, text="Veuillez entrer les données de l'animal", font=("Consolas", 13), bg="#C2C2C2", fg="black")
soustitre_ajoute.pack()
desc_chmp_nom_str_ajoute = Label(cadre_ajoute, text="entrez son nom", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_nom_str_ajoute.pack()
chmp_nom_str_ajoute = Entry(cadre_ajoute, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp_nom_str_ajoute.pack()
desc_chmp_espece_str_ajoute = Label(cadre_ajoute, text="entrez son espèce", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_espece_str_ajoute.pack()
chmp_espece_str_ajoute = Entry(cadre_ajoute, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp_espece_str_ajoute.pack()
desc_chmp_race_str_ajoute = Label(cadre_ajoute, text="entrez sa race", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_race_str_ajoute.pack()
chmp_race_str_ajoute = Entry(cadre_ajoute, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp_race_str_ajoute.pack()
desc_chmp_age_str_ajoute = Label(cadre_ajoute, text="entrez son âge", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_age_str_ajoute.pack()
chmp_age_str_ajoute = Entry(cadre_ajoute, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp_age_str_ajoute.pack()
desc_chmp_localisation_str_ajoute = Label(cadre_ajoute, text="entrez le lieu où il a été trouvé", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_localisation_str_ajoute.pack()
chmp_localisation_str_ajoute = Entry(cadre_ajoute, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp_localisation_str_ajoute.pack()

#fonction pour ajouter les donnée choisies dans les champs de texte de la page ajouter dans la base de donnéee
def ajouter_donnee():
    connection = sqlite3.connect('base_SPA.db')
    curseur = connection.cursor()
    data = (chmp_nom_str_ajoute.get(),chmp_espece_str_ajoute.get(),chmp_race_str_ajoute.get(),chmp_age_str_ajoute.get(),chmp_localisation_str_ajoute.get())
    curseur.execute("INSERT INTO ANNIMAL (nom,espèce,race,age,trouvé_où) VALUES (?,?,?,?,?)",data)
    connection.commit()
    curseur.close()
    connection.close()

# création et placement du bouton utiliser pour valider l'ajout de donnée
bouton_ajouter = Button(cadre_ajoute, text="Ajouter l'annimal", font=("Consolas",15), bg="white", fg="black",command = ajouter_donnee)
bouton_ajouter.pack(pady=20)

Dans ce code, nous créons la fenêtre Ajouter avec les informations à remplir. Lorsque l’on clique sur le bouton Ajouter l’animal, celui ci sera ajouté automatiquement à la table de données.

# création des different blocs + affichage des blocs
titre_modifier = Label(cadre_modifier, text="MODIFIER", font=("Consolas", 20), bg="#C2C2C2", fg="black")
titre_modifier.pack(pady=20)
desc_chmp_modifier = Label(cadre_modifier, text="entrez l'id de l'annimal", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp_modifier.pack()
chmp_str_modifier = Entry(cadre_modifier, font=("Helvetica",15), bg="#ffffff", fg="black", width=5)
chmp_str_modifier.pack()

# création de la liste déroulante
desc_liste_deroul_modifier = Label(cadre_modifier, text="Choissisez quel donnée voulez vous modifier", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_liste_deroul_modifier.pack(pady=10)
liste_choix = ["Nom","Race","Espèce","Age","Localisation"]
liste_deroulante = ttk.Combobox(cadre_modifier, values=liste_choix)
liste_deroulante.pack()

desc_chmp2_modifier = Label(cadre_modifier, text="entrez la modification", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_chmp2_modifier.pack(pady=10)
chmp2_str_modifier = Entry(cadre_modifier, font=("Helvetica",15), bg="#ffffff", fg="black", width=30)
chmp2_str_modifier.pack()

# fonction pour modifier une donnée dans la base de donnée à partir de la liste déroulante et le champ de texte
def modifier_donnee():
    select = liste_deroulante.get()
    datas = (chmp2_str_modifier.get(),chmp_str_modifier.get())
    connection = sqlite3.connect('base_SPA.db')
    curseur = connection.cursor()
    if select == "Nom":
        curseur.execute("UPDATE ANNIMAL SET nom = ? WHERE id = ?",datas)
    if select == "Race":
        curseur.execute("UPDATE ANNIMAL SET race = ? WHERE id = ?",datas)
    if select == "Espèce":
        curseur.execute("UPDATE ANNIMAL SET espèce = ? WHERE id = ?",datas)
    if select == "Age":
        curseur.execute("UPDATE ANNIMAL SET age = ? WHERE id = ?",datas)
    if select == "Localisation":
        curseur.execute("UPDATE ANNIMAL SET trouvé_où = ? WHERE id = ?",datas)
    connection.commit()
    curseur.close()
    connection.close()

# fonction pour supprimer toutes les données lié à un identifiant dans la base de donnée   
def supprimer_donnee():
    sup_select = chmp_str_modifier.get()
    connection = sqlite3.connect('base_SPA.db')
    curseur = connection.cursor()
    curseur.execute("DELETE FROM ANNIMAL WHERE id = ?",sup_select)
    connection.commit()
    curseur.close()
    connection.close()

# création et placement des boutons pour valider les modifications / suppressions
bouton_sup = Button(cadre_modifier, text="Supprimer l'annimal ayant cet id", font=("Consolas",15), bg="white", fg="black", command = supprimer_donnee)
bouton_sup.pack(pady=30)
bouton_modifier = Button(cadre_modifier, text="Modifier", font=("Consolas",15), bg="white", fg="black", command = modifier_donnee)
bouton_modifier.pack()

La fenêtre modifier quant à elle a besoin de l‘id de l’animal. En effet, pour modifier une information sur un animal, la personne doit rentrer l’id présente sur la table de donnée correspondant à l’animal, après cela, elle choisit l‘information à modifier dans la liste déroulante puis rentre la correction à faire. Ensuite, cette fenêtre abrite aussi la possibilité de supprimer l’animal comme cela a été dit précédemment grâce à la fonction supprimer_donnee() par l’id de l’animal.

# création des different blocs + affichage des blocs
titre_parcourir = Label(cadre_parcourir, text="PARCOURIR", font=("Consolas", 20), bg="#C2C2C2", fg="black")
titre_parcourir.pack(pady=10)
desc_liste_deroulante_parcourir = Label(cadre_parcourir, text="Choississez par qu'elle type de donnee souhaitez vous rechercher", font=("Consolas", 13), bg="#C2C2C2", fg="black")
desc_liste_deroulante_parcourir.pack(pady=10)

# création de la liste déroulante du type de donnée
liste_deroulante_parcourir = ttk.Combobox(cadre_parcourir, values=["id","Nom","Espèce", "Age","Race","Localisation"])
liste_deroulante_parcourir.set("id")
liste_deroulante_parcourir.pack(pady=10)

# création du cadre blanc où apparaissent les recherches effectuées
frame_info_recherche = Frame(cadre_parcourir, bg="#C2C2C2",heigh = 50,width=650)
frame_info_recherche.pack_propagate(False) 
frame_info_recherche.pack()

desc_chmp_str_parcourir = Label(frame_info_recherche, text="écrivez cette information de recherche", font=("Consolas", 10), bg="#C2C2C2", fg="black")
desc_chmp_str_parcourir.place(x=0,y=0)

chmp_str_parcourir = Entry(frame_info_recherche, font=("Helvetica",10), bg="#ffffff", fg="black", width=15)
chmp_str_parcourir.place(x=0,y=23)

desc_chmp_str_parcourir = Label(frame_info_recherche, text="Si c'est une valeur elle doit être :", font=("Consolas", 10), bg="#C2C2C2", fg="black")
desc_chmp_str_parcourir.place(x=390,y=0)

# création de la liste déroulante pour préciser la recherche par valeurs numériques
liste_deroulante_info_recherche = ttk.Combobox(frame_info_recherche, values=["=","<=",">=","<",">"],width=15)
liste_deroulante_info_recherche.set("=")
liste_deroulante_info_recherche.place(x=390,y=23)

variable = StringVar()
variable.set("1")
chmp_str_info_recherche_parcourir = Entry(frame_info_recherche,textvariable=variable, font=("Helvetica",10), bg="#ffffff", fg="black", width=15)
chmp_str_info_recherche_parcourir.place(x=510,y=23)

# création de la variable qui stocke tout le résultat d'une recherche
global recherche
recherche = ""

# fonction pour construire notre variable
def parcourir_donnee():
    global recherche
    recherche = ""
    connection = sqlite3.connect('base_SPA.db')
    curseur = connection.cursor()
    data = [chmp_str_parcourir.get()]
    data2 = [chmp_str_info_recherche_parcourir.get()]
    if liste_deroulante_parcourir.get() == "Nom":
        recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE nom = ?",data).fetchall()
    if liste_deroulante_parcourir.get() == "Race":
        recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE race = ?",data).fetchall()
    if liste_deroulante_parcourir.get() == "Espèce":
        recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE espèce = ?",data).fetchall()
    if liste_deroulante_parcourir.get() == "Localisation":
        recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE trouvé_où = ?",data).fetchall()
    if liste_deroulante_parcourir.get() == "Age":
        if liste_deroulante_info_recherche.get() == "=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE age = ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == "<":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE age < ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == ">":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE age > ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == "<=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE age <= ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == ">=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE age >= ?",data2).fetchall()
    if liste_deroulante_parcourir.get() == "id":
        if liste_deroulante_info_recherche.get() == "=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE id = ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == "<":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE id < ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == ">":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE id > ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == "<=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE id <= ?",data2).fetchall()
        elif liste_deroulante_info_recherche.get() == ">=":
            recherche = curseur.execute("SELECT nom,espèce,race,age,trouvé_où FROM ANNIMAL WHERE id >= ?",data2).fetchall()
    connection.commit()
    curseur.close()
    connection.close()

# fonction pour afficher la recherche sous une forme lisible
def texte_parcourir():
    texte_widget_frame_parcourir.delete("1.0","end")
    parcourir_donnee()
    for car in str(recherche):
        if car == "[" or car == "]" or car == "(" or car == "'"  :
            texte_widget_frame_parcourir.insert(INSERT,"")
        elif car == ",":
            texte_widget_frame_parcourir.insert(INSERT,"")
        elif car == ")":
            texte_widget_frame_parcourir.insert(INSERT,"\n")
        else :
            texte_widget_frame_parcourir.insert(INSERT,car)
  
# création et placement du bouton pour valider une recherche
bouton_parcourir = Button(cadre_parcourir, text="Rechercher", font=("Consolas",15), bg="white", fg="black",command = texte_parcourir)
bouton_parcourir.pack(pady=10)


frame_parcourir = Frame(cadre_parcourir, bg="#ffffff",heigh = 210,width=650)
frame_parcourir.pack_propagate(False) 
frame_parcourir.pack()

# création et placement de la barre de scroll lié à la recherche effectuée
scrollbar = Scrollbar(frame_parcourir, orient=VERTICAL)
scrollbar.pack(side=RIGHT, fill=Y)

texte_widget_frame_parcourir = Text(frame_parcourir, yscrollcommand=scrollbar.set)
texte_widget_frame_parcourir.pack(fill=BOTH, expand=True)

scrollbar = Scrollbar(texte_widget_frame_parcourir)

scrollbar.config(command=texte_widget_frame_parcourir.yview)

texte_recherche_parcourir = Text(frame_parcourir)

Dans ce code, nous créons la fenêtre Parcourir avec toutes ses informations.

Puis, finalement, on peut terminer le script :

# Affichage de la page d'accueil au lancement du script
cadre_acceuil.pack()

# Création du menu
menubar = Menu(fenetre)

# Ajout des commandes directement au menu principal
menubar.add_command(label="Acceuil", command=affiche_acceuil)
menubar.add_command(label="Ajouter", command=affiche_ajoute)
menubar.add_command(label="Modifier", command=affiche_modifier)
menubar.add_command(label="Parcourir", command=affiche_parcourir)

# Configuration de la fenêtre avec la barre de menus
fenetre.config(menu=menubar)

# Initialisation de la pile des cadres
cadre_pile = [cadre_acceuil]

fenetre.mainloop()

Cette fin de script permet d’afficher l’interface et ses informations. Sans cela, Le script ne serait pas utilisable

III – Conclusion

Ainsi, ce projet nous a permis de mobiliser nos bases acquises en python et SQL pour monter un projet traitant sur les bases de données. Ce type de projet n’est pas des plus simples compte tenu de la technique à avoir et des difficultés rencontrées telles que le positionnement des blocs et les interactions avec la base de données. Pour continuer, il est important dans ce type de projets de comprendre son code ligne à ligne afin de ne pas se tromper sur un nom, une valeur ou autres.

Projets

La version améliorée de PixChoice

PixChoice est un site créé par Vincent ROBERT, Kevin FEDYNA et Ilyas R. Le site affiche 6 images de manières aléatoires parmi une liste afin de pouvoir voter pour la meilleure proposition. Le site calcule par la suite les résultats et permet de les afficher. Et j’en ai fait une version améliorée pour faciliter la modification des paramètres ou des utilisateurs notamment et je vais la présenter.

L’idée

Comme dit dans l’extrait le site propose 6 images parmi une liste et vous permet de voter pour celle qui vous plaît le plus. Cependant lors de sa V1 le site ne permettait pas un changement de paramètre optimal ce qui obligeait de se souvenir de l’utilisation de l’année passé et ce qui était une perte de temps, j’ai donc créé la V2 qui ajoute un interface administrateur qui permet de paramétrer les images, le vote et l’affichage des résultats. Pour cela je me suis appuyé sur le code de la V1 trouvable ici ainsi que le framework CSS PureCSS.io (Un framework CSS est un ensemble prédéfini de styles et de composants prêts à l’emploi pour faciliter le développement web). C’est ainsi que j’ai pu concevoir la V2 de PixChoice

Quelques images du site

Vous avez donc ici quelques images du site de la vue d’un utilisateur ne possédant pas de compte et qui ne peut accéder qu’à la page de vote, aux résultats et à la page de connexion.

Fonctionnement

Je vais vous faire une brève explication du fonctionnement du site :

Les différentes pages du site sont faites en php c’est à dire que dans la page écrite en html on peut y ajouter du code, des fonctions… C’est ainsi que les pages du site sont dynamiques et s’affichent selon les paramètres. Et en parlant de paramètres, il faut bien les stocker mais où ? Pour stocker les informations, une base de donnée à été mise à ma disposition afin de sauvegarder des informations comme les utilisateurs avec leur identifiant et leur mot de passe, les images avec leur nombre de vote et combien de fois elles sont apparues, l’adresse ip des votants et la configuration du site. Et comme le php permet nativement de faire des requêtes sql l’utilisation de la base de donnée est simplifiée.

Dans les fonctionnalitées de php on peut également inclure d’autres page, c’est à dire que le code de la page donnée en paramètre va être placé au niveau de l’appel via la fonction ce qui me permet de n’avoir que 3 pages principales, c’est à dire que seulement 3 pages vont être les fondations du sites et les autres pages vont seulement remplir les 3 pages principales. On le retrouve notamment dans le code de la page index.php

<?php
session_start(); // Création d'une session pour sauvegardé des données
// Appel de différentes pages pour instancier ...
require_once(__DIR__ . '/config.php'); // Les identifiants de la base de donnée
require_once(__DIR__ . '/databaseconnect.php'); // Une connexion à la base de donnée
require_once(__DIR__ . '/variables.php'); // Des variables récupérées en sql
require_once(__DIR__ . '/fonctions.php'); // Des fonctions

cookie($temps_vote[0][0]); // Appel de la fonction cookie qui va créer un cookie 
//pour sauvegarder le nombre de vote fait
?>

<!doctype html> <!-- Code html basique -->
<html lang="fr">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pixchoice</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>

<?php // retour en php pour récupérer les variables envoyés en post ou en get
$postData = $_POST;
$getData = $_GET;

include('header.php'); // Ajout de l'en tête de page

// ajout de la page de connexion si des variables existent
if (isset($postData['connexion']) || isset($_GET['connexion'])) { 
    include('login.php');
} else {
    include('vote.php'); // et si elles n'existent pas ajout de la page de vote
};?>
</body> <!-- Fin du code html -->
</html>

Vous remarquerez donc que mes pages ne sont pas bien conséquentes comme je peux créer d’autres pages pour les ajouter directement dans le code d’une autre, et c’est pourquoi j’ai 3 pages principales mais 19 pages secondaires qui sont appelées par les pages principales.

Fonctionnalités

Voici donc les différentes fonctionnalités proposées par le site afin de faciliter l’utilisation du site par les administrateurs :

  • Configuration du vote
    • Activation / Désactivation du vote.
    • Activation / Désactivation du vote pour les admins malgré la limite. (permet aux administrateurs de voter sans limites)
    • Gestion du nombre de vote avant d’être bloqué
    • Gestion du temps avant d’être débloqué
  • Configuration des images
    • Ajout d’image (avec gestion de formatage du nom et d’id dans la db)
    • Suppression d’images (avec gestion dans la db)
  • Configuration des résultats
    • Activation / Désactivation des résultats pour tous.
    • Activation / Désactivation de l’affichage des résultats pour les admins.
  • Gestion des utilisateurs
    • Ajout d’utilisateurs avec génération de mot de passe temporaire aléatoire
    • Tableau référençant les utilisateurs qui permet d’en supprimer ou d’activer ou non le fait qu’ils soient superadmin (seul les superadmin ont accès à cette page)
  • Configuration du compte
    • Modification de l’identifiant
    • Modification du mot de passe

Configuration du vote

Afin de configurer le vote plusieurs paramètres sont pris en charges:

  • Activation / Désactivation du vote.
  • Activation / Désactivation du vote pour les admins malgré la limite. (permet aux administrateurs de voter sans limites)
  • Gestion du nombre de vote avant d’être bloqué
  • Gestion du temps avant d’être débloqué

Voici donc la page permettant de configurer le vote

Configuration des images

Afin de configurer le vote plusieurs paramètres sont pris en charges:

  • Ajout d’image (avec gestion de formatage du nom et d’id dans la db)
  • Suppression d’images (avec gestion dans la db)

Voici donc la page permettant de configurer les images

Configuration des résultats

Afin de configurer le vote plusieurs paramètres sont pris en charges:

  • Activation / Désactivation des résultats pour tous.
  • Activation / Désactivation de l’affichage des résultats pour les admins.

Voici donc la page permettant de configurer les résultats

Gestion des utilisateurs

Afin de configurer le vote plusieurs paramètres sont pris en charges:

  • Ajout d’utilisateurs avec génération de mot de passe temporaire aléatoire
  • Tableau référençant les utilisateurs qui permet d’en supprimer ou d’activer ou non le fait qu’ils soient superadmin (seul les superadmin ont accès à cette page)

Voici donc la page permettant de gérer les utilisateurs

Configuration du compte

Afin de configurer le vote plusieurs paramètres sont pris en charges:

  • Modification de l’identifiant
  • Modification du mot de passe

Voici donc la page permettant de configurer son compte

Conclusion

Pour conclure le tout ce projet a été très enrichissant et très exigeant mais je me suis bien amusé à le faire, j’ai pu découvrir toute une partie du développement web étant donné que j’ai dû apprendre le php mais je m’en suis sorti et je suis plutôt fier du résultat.

Dépôt Github

Vous pouvez retrouver les fichiers sources du projet sur mon dépôt Github afin de mettre en place mon site sur votre serveur.

EDIT de l’enseignant : Le projet initial est un code de Julien Bernon, enseignant de physique-chimie et spécialité NSI. Merci à lui de nous avoir permis de reprendre son projet initial.

Projets

Application de gestion de playlist (SGBDR) avec Python et…

Dans le monde numérique d’aujourd’hui, la musique est omniprésente, et la possibilité de gérer efficacement ses playlists est essentielle pour tout amateur de musique. Dans cet article, nous allons explorer comment créer une application simple de gestion de playlist en utilisant Python et Tkinter, deux outils puissants et accessibles pour le développement d’interfaces graphiques.

Introduction à Tkinter et SQLite

Tkinter est une bibliothèque standard de Python largement utilisée pour créer des interfaces graphiques. Elle offre une variété de widgets (éléments d’interface utilisateur) tels que des boutons, des étiquettes et des cadres, permettant de construire des applications interactives.

SQLite est une bibliothèque légère de gestion de base de données relationnelle souvent intégrée dans les applications pour stocker des données de manière structurée. Dans notre cas, nous l’utiliserons pour stocker les informations sur notre playlist musicale.

Création de la structure de données

Nous allons commencer par définir la structure de notre base de données SQLite, qui stockera les détails de chaque morceau de musique dans notre playlist. Nous aurons des champs tels que le titre de la chanson, l’année de sortie, la durée, le genre, l’artiste et le type d’album.

# Création de la table dans la base de données SQLite
def create_table():
    conn = sqlite3.connect("playlist.db")
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS playlist (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            titre TEXT,
            annee INTEGER,
            duree TEXT,
            genre TEXT,
            artiste TEXT,
            album_type TEXT
        )
    ''')
    conn.commit()
    conn.close()

Dans cette fonction create_table(), nous établissons une connexion à notre base de données SQLite et exécutons une requête SQL pour créer une table nommée « playlist » avec les champs spécifiés. Cette fonction vérifie d’abord si la table existe déjà avant de la créer, grâce à la clause IF NOT EXISTS.

Lecture des données de la playlist

Une fois que la structure de la table est définie, nous pouvons écrire une fonction pour lire les données de la table et les afficher. Voici comment cela peut être fait :

# Lecture des données de la table et affichage
def read_data():
    conn = sqlite3.connect("playlist.db")
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM playlist")
    rows = cursor.fetchall()
    conn.close()

    display_text = ""
    for row in rows:
        display_text += f"ID: {row[0]}, Titre: {row[1]}, Année: {row[2]}, Durée: {row[3]}, Genre: {row[4]}, Artiste: {row[5]}, Type d'album: {row[6]}\n"

    text_display.config(state=tk.NORMAL)
    text_display.delete('1.0', tk.END)
    text_display.insert(tk.END, display_text)
    text_display.config(state=tk.DISABLED)

Dans la fonction read_data(), nous établissons à nouveau une connexion à la base de données, exécutons une requête SQL pour sélectionner toutes les entrées de la table « playlist » et les récupérons à l’aide de fetchall(). Ensuite, nous bouclons à travers chaque ligne et les affichons.

Interface Graphique avec Tkinter

Maintenant que nous avons mis en place la gestion des données, nous pouvons créer une interface graphique conviviale pour notre application de gestion de playlist.

# Création de la fenêtre principale
window = tk.Tk()
window.title("Gestion de Playlist")
window.geometry("780x720")
window.minsize(680, 360)
window.configure(bg="#000")

Nous avons créé une fenêtre principale en utilisant tk.Tk() et lui avons donné un titre avec window.title(). Nous avons également défini la géométrie de la fenêtre et spécifié une couleur de fond.

Création des boutons d’action

Ensuite, nous avons ajouté des boutons pour effectuer différentes actions telles que la création de la table, la lecture des données et la manipulation des entrées de la playlist.

# Création des boutons d'action
frame_buttons = tk.Frame(window, bg="#000")
frame_buttons.pack(side=tk.TOP, pady=20)

btn_create = tk.Button(frame_buttons, text="CREATE", font=("Courrier", 14), bg="#01D758", fg="#000", command=create_table)
btn_create.pack(side=tk.LEFT, padx=10)

btn_read = tk.Button(frame_buttons, text="READ", font=("Courrier", 14), bg="#01D758", fg="#000", command=read_data)
btn_read.pack(side=tk.LEFT, padx=10)

Nous avons utilisé la classe Button de Tkinter pour créer des boutons, spécifiant leur texte, leur police, leur couleur de fond et de texte, ainsi que les actions qu’ils effectueront lorsqu’ils seront cliqués.

Conclusion

Dans cet article, nous avons exploré la création d’une application simple de gestion de playlist en utilisant Python et Tkinter. Nous avons vu comment créer une structure de base de données SQLite pour stocker les informations sur les morceaux de musique, ainsi que comment concevoir une interface graphique conviviale pour interagir avec ces données. Avec cette base, les amateurs de musique peuvent commencer à construire leur propre application de gestion de playlist personnalisée.

Projets

Un météorologue personnel en briquette !

Qui n’a jamais rêvé d’avoir un robot en LEGO qui lui annonce la météo ? Et bien même si ce n’est pas le cas, nous l’avons fait pour vous ! Venez découvrir un robot capable de comprendre son environnement pour vous donner la météo.

Fonctionnalités du robot

Notre robot au doux nom de ROBKEY est capable de :

  • déplacer ses bras et son bassin ;
  • reconnaitre l’endroit où il se trouve pour donner précisément la météo ;
  • afficher l’heure, la météo et la température ambiante ;
  • d’avoir quelques réactions faciales ;
  • et bien plus.

La conception du robot

Pour concevoir notre robot, nous avons dû prendre en compte plusieurs choses. Tout d’abord, les moteurs n’ont pas été particulièrement dur à installer car il ne font que la moitié de la taille de l’esp 32. Il nous a donc paru évident de mettre les moteurs des bras dans le torse du robot et le moteur du bassin dans le châssis de ses chenilles.

Ensuite il a fallu installer planche de test qui a eu plusieurs positions possibles. Premièrement dans son dos car cela aurait permis de pouvoir faire tourner le bassin du robot à 360°, mais les câbles auraient difficilement bien tenu à l’esp 32 et au robot lui-même. Nous nous sommes finalement entendu à l’idée de mettre la planche sur la batterie des chenilles bien que cela empêche le robot de faire des tours sur lui même.

Maintenant, le plus important : le placement de l’ESP32. Nous avions deux idées, l’une le plaçait dans le torse du robot, l’autre faisait en sorte qu’il soit la tête légèrement rentrée dans les épaules. Nous avons opté pour la seconde option car celle-ci nous permet d’afficher un petit visage sur l’écran du microcontrôleur.

Pour conceptualiser l’apparence de notre robot, nous avons donc réalisé un schéma en 3D très simple sur Paint 3D en regroupant nos idées pour imaginer ce à quoi il ressemblera.

Construction de la structure du robot

Pour construire le robot, j’ai (Sylvain) commencé par regarder les constructions Lego que j’avais précédemment réalisées et qui pouvaient être détruite pour commencer à récupérer leurs pièces. Avec toutes ses matières premières je me suis laissé porter par mon imagination pour construire le LEGO le plus ressemblant à un robot en essayant de faire en sorte qu’il soit plutôt résistant (bon d’accord j’ai peut-être failli à cette tâche).

C’est grâce aux joies de la maladie que j’ai pu entamer la construction. Pour commencer, j’ai récupéré le châssis d’une autochenille radiocommandée en la modifiant pour la rendre plus compacte et en laissant un espace pour le moteur du bassin.

Ensuite j’ai commencé à faire ledit bassin, que j’élargis à la fin de la construction car je l’avais fait trop fin. Suivi du torse, le plus simple car il devait être creux pour laisser la places aux câbles et aux moteurs. Il fallait aussi préparer les trous pour les axes des bras.

Après j’ai fait les bras qui sont vraiment très simplistes mais à ce moment je commençais sérieusement à manquer de pièces plus techniques et même des plus simples pour faire quelque chose de plus sympathique. Mais quoi qu’il en est l’un des deux bras représente un soleil pour quand il fait beau, tandis que l’autre représente un ciel nuageux.

Pour finir j’ai réalisé la cage de la planche de test et solidifié les éléments les moins solides et résolu les quelques problèmes mécaniques rencontrés lors des premiers tests des moteurs.

L’électronique du robot

En parallèle de la construction du LEGO, je (Thomas) me suis lancé dans la conception électronique de celui-ci.

Pour cela, j’ai d’abord listé tout ce que je voulais intégrer à l’électronique :

  • 2 boutons,
  • 1 capteur de température,
  • 3 moteurs.

Pour m’aider dans la réalisation électronique, je me suis aidé de tous ces sites :

Allez c’est parti !

On commence par le branchement des boutons :

Les câbles blancs (entourés en rouge) récupèrent l’information de la pression du bouton et les câbles noirs (entourés en vert) s’occupe de fermer le circuit (de l’électricité passent dans le bouton, et il faut que l’électricité puisse faire une boucle). Si vous voulez, les boutons reçoivent de l’énergie par les câbles blancs et l’expulsent par les câbles noirs.

Concernant le branchement du capteur :

A droite à quoi ressemble le capteur de température de face et à gauche son branchement. La câble de gauche, en blanc récupère les mesures du capteur, celui du milieu en marron, alimente positivement le capteur (le capteur reçoit de l’énergie par ce câble) et le câble noir à droite du câble marron lui ferme la boucle (le capteur expulse l’énergie par ce câble.)

Et pour finir, les moteurs ont 3 câbles :

Le câble orange sert à contrôler le moteur, le rouge à l’alimenter et le marron à fermer la boucle. On refait ça 3 fois puisqu’on a 3 moteurs.

On branche tout ceci à la planche de test puis à l’ESP32, et voilà montage terminé :

Programmation du robot

Maintenant que le robot est construit et le montage électronique assemblé, il faut passer à la programmation de l’ESP32. Veuillez noter que depuis l’écriture de ce qui suit, le code a bénéficié de quelques mises à jours. Vous retrouverez l’archive en fin d’article avec les fichiers à jours.

Tout d’abord, il faut coder les briques de bases, qui vont nous permettre de manipuler différents outils, capteurs, actionneurs, api, etc.

  • C’est pour cela que nous créons un fichier buttons.py qui contient une classe Button dans laquelle nous initialisons 4 boutons : les 2 du microcontrôleur et les deux boutons ajoutés. Dans cette classe, vous verrez que chaque boutons a des caractéristiques supplémentaires. En effet il est possible de rendre un bouton poussoir équivalent à un levier. Nous créons différentes fonctions pour récupérer ou modifier des informations en rapport avec les boutons.
from machine import Pin
from time import sleep

# Importer les modules est inutile car ils sont déjà importés dans boot.py

class Buttons():
    def __init__(self):
        #self.name = "t-display-s3"
        
        # NomDuBouton : [                    PinUtilisé                     ,    mode    , value ]
        #      1      : [ Pin 3 en mode INPUT avec une résistance Pull-Down ,   levier   ,   0   ]
        # 1 : [ Pin(3, mode=Pin.IN, pull=Pin.PULL_DOWN), 1, 0 ]
        
        # Note : le chiffre de value dans la liste n'est pas pris en compte si le bouton est en mode "poussoir" -> On ne peut pas mettre None car si on décide de changer le mode du bouton, la valeur devient importante.
        
        
        # Pour une raison inconnue, "left" et "right" ne fonctionne plus.
        self.list_button = {"left" : [Pin(0, Pin.IN, Pin.PULL_UP),0,1],
                            "right" : [Pin(14, Pin.IN, Pin.PULL_UP),0,1],
                            1 : [Pin(2, mode=Pin.IN, pull=Pin.PULL_UP),0,1],
                            2 : [Pin(3, Pin.IN, Pin.PULL_UP),0,1]
                            }


    def getButton(self, button):
        return self.list_button[button]
    
    def setButton(self, button, pin, mode, value=None): # Le set fait également add.
        self.list_button[button] = [pin,mode,value]

    def setMode(self, button, val:bool): # Ce mode change le mode du bouton -> soit "levier" soit "poussoir"
        if isinstance(val, bool) or val in (0,1):
            self.list_button[button][1] = val
            if not(val):
                self.list_button[button][2] = 1
            return print("Mode définie sur : " + str(self.list_button[button][1]))
        else:
            raise ValueError("val must be a boolean or a integer included between 0 and 1.")
    
    def setValue(self, button, value):
        if self.list_button[button][1]:
            self.list_button[button][2] = value
            return print("Valeur définie sur : " + str(self.list_button[button][2])) 
        else:
            print("Button must be a toogle button, not a push button.")
            return False
    
    def getValue(self, button): # Renvoie la valeur du bouton
        if not(self.list_button[button][1]): # Si le statut de mode_toogle est égal à 0 <=> Si le bouton est en mode "poussoir" et non "levier"
            return self.list_button[button][0].value()
        else:
            if not(self.list_button[button][0].value()):
                self.list_button[button][2] = 0 if self.list_button[button][2] else 1
                sleep(0.25)
            return self.list_button[button][2]
    
    def isButtonPressed(self,button): # Renvoie True si le bouton est pressé (donc si value = 0) et False sinon
        return not(self.getValue(button))
  • Continuons avec le fichier temperature.py dans lequel nous avons une simple fonction qui nous renvoie les données récupérées par le capteur de température et d’humidité.
from time import sleep  #Inutile car importé dans boot.py

def getTemperatureAndHumidity(capteur): # Capteur doit etre sous cette forme : capteur = dht.DHT11(Pin(17)) -> Ne pas oublier d'importer le module dht
    try:
        capteur.measure() # Met à jour les données de températures et d'humidités
        sleep(1)
        return (capteur.temperature(),capteur.humidity())
    except:
        print("Valeurs non récupérées")
        return False
  • Egalement, à l’aide des fichiers motor.py et servo.py (trouvé sur internet), nous créons des fonctions qui réaliseront des déplacements particuliers :
from servo import Servo
import time

motor=Servo(pin=12) # A changer selon la broche utilisée
motorbd=Servo(pin=10)
motorbg=Servo(pin=11)
 
def droit(t=1):
    motor.move(285)
    time.sleep(t)

def bassin(t=2):
    motor.move(265)
    time.sleep(t)
    motor.move(305)
    time.sleep(t)

def baissebg(t=0.5):
    motorbg.move(335)
    time.sleep(t)
    
def levebg(t=0.5):
    motorbg.move(270)
    time.sleep(t)

def baissebd(t=0.5):
    motorbd.move(335)
    time.sleep(t)
    
def levebd(t=0.5):
    motorbd.move(270)
    time.sleep(t)
    
droit()
  • Pour finir, à l’aide du fichier connection.py, nous créons plusieurs fonctions en rapport avec la partie sans-fil de l’ESP32.
import network
import st7789 #En théorie inutile car déjà importé dans boot.py
from time import sleep

def connect_to_wifi(display, font, ssid, mdp):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        display.init()
        display.draw(font,"Connexion a un point",0,111)
        display.draw(font,"d'acces en cours",15,126)
        wlan.connect(ssid,mdp)
        while not wlan.isconnected():
            pass
    display.fill_rect(0,107,170,25,st7789.BLACK)
    display.draw(font,'Connecte !',40,111)
    sleep(0.8)
    display.fill_rect(0,107,170,10,st7789.BLACK)
    display.deinit()

def search_wlan():
    station = network.WLAN(network.STA_IF)
    station.active(True)
    return station.scan()

Passons aux modules qui vont utiliser des apis :

  • En premier lieu, nous avons le fichier geoloc.py dans lequel nous utilisons l’api de google qui à l’aide des réseaux wifi aux alentours est capable de situer l’ESP32. Dans ce fichier est présent une fonction qui récupère les coordonnées géographiques de l’ESP32.
from modules.connection import search_wlan
import ustruct as struct
import urequests as requests
import ujson as json
import modules.api_txt as api_txt


def getLocation(api_key): # Avec Google maps API + sécurité sur le nombre de requête.    
    
    if int(api_txt.get_api_counter()[0]) > 24000:
        print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...")
        return False
    
    list_wlan = search_wlan()
    data = {
        "considerIp": False,
        "wifiAccessPoints": []
    }

    for wifi in list_wlan:
        entry = {
            "macAddress": "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", wifi[1]),
            "signalStrength": wifi[3],
            "channel": wifi[2]
        }
        data["wifiAccessPoints"].append(entry)
    
    headers = {"Content-Type": "application/json"}
    url = "https://www.googleapis.com/geolocation/v1/geolocate?key=" + api_key
    response = requests.post(url, headers=headers, data=json.dumps(data))

    api_txt.add_api_counter(0)

    return json.loads(response.content)["location"]
  • En second et dernier lieu, il reste le fichier meteo.py qui va être capable d’utiliser une api de météorologie pour récupérer, analyser et mettre en forme des données en rapport avec la météo.
import urequests
import modules.api_txt as api_txt

def get_meteo(latitude, longitude): # Sur la prochaine heure
    
    if int(api_txt.get_api_counter()[1]) > 10000:
        print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...")
        return False
    
    url = "https://api.open-meteo.com/v1/forecast?latitude={0}&longitude={1}&current=temperature_2m,precipitation&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto&forecast_days=3".format(latitude,longitude)
    json_meteo = urequests.get(url).json()
    
    api_txt.add_api_counter(1)
    
    return {"current":
            {"temperature":
             [
                 json_meteo["current"]["temperature_2m"],
                 json_meteo["current_units"]["temperature_2m"]
                 ],
             "precipitation":
             [
                 json_meteo["current"]["precipitation"],
                 json_meteo["current_units"]["precipitation"]
                 ]
             },
            "demain":
            {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][1] + json_meteo["daily"]["temperature_2m_max"][1]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]],
             "precipitation":
             [
                 json_meteo["daily"]["precipitation_sum"][1],
                 json_meteo["daily_units"]["precipitation_sum"]
                 ]
             },
            "apres-demain":
            {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][2] + json_meteo["daily"]["temperature_2m_max"][2]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]],
             "precipitation":[
                 json_meteo["daily"]["precipitation_sum"][2],
                 json_meteo["daily_units"]["precipitation_sum"]
                 ]
             }
            }

Passons à la gestion graphique, nous avons récupérer le module tft_config.py fournit par les développeurs du pilotes graphiques pour l’esp32. Celui-ci permet de créer facilement une gestion de l’écran avec les bonnes caractéristiques. Nous n’afficherons pas le code ici, car ce n’est pas nous qui l’avons codé. Vous pouvez le retrouver dans l’archive à la fin de cet article ou ici.

Nous avons créé le fichier affichage.py qui contient un certain nombre de fonctions pour réaliser le rendu graphique pour l’ESP32. Il contient également le code qui récupère des images pour les animations du visage et des icones. Ces dessins et animations ont été réalisé par Ilana, ancienne première NSI.

import st7789
from time import sleep, localtime
import modules.motor as motor

last_temps = -1

def display_time(display,font,x,y):
    global last_temps
    temps = localtime()
    if last_temps < temps[5]:
        display.png("/images/icones/horloge.png",x,y,True)
        display.fill_rect(x+30,y+5,60+6,10,st7789.BLACK)
        display.draw(font, str(temps[3]) + ":" + str(temps[4]) + ":" + str(temps[5]),x+30,y+10)
    last_temps = temps[5]

def display_meteo(display,font,infos_meteo, x, y):
    display.draw(font,str(infos_meteo["temperature"][0]),x,y)
    display.draw(font, font.LAST,x + display.draw_len(font,str(infos_meteo["temperature"][0])), y)
    display.draw(font, " C",x + display.draw_len(font,str(infos_meteo["temperature"][0])), y)
    
    display.draw(font,str(infos_meteo["precipitation"][0]),x+80,y)
    display.draw(font,str(infos_meteo["precipitation"][1]),x + 80 + display.draw_len(font,str(infos_meteo["precipitation"][0])),y)

def display_temperature(display,font,infos_temperature,x,y):
    display.fill_rect(x,y,70,20,st7789.BLACK)
    display.png("/images/icones/temperature.png",x,y,True)
    display.draw(font,str(infos_temperature) + " C",x+30,y+9)
    display.draw(font,font.LAST,x+30+display.draw_len(font,str(infos_temperature)), + y+9)    
    
def meteo_icon(display,icone,x,y):
    display.fill_rect(x,y,50,50,st7789.BLACK)
    if icone == "pluie":
        display.png("/images/icones/mauvais_temps.png",x,y,True) #Nuage pluie gris pas content
    else:
        display.png("/images/icones/beau_temps.png",x,y,True) # Soleil content
        

def meteo_widget(display,font,infos_meteo,x,y):
    display.fill_rect(x,y,170,200,st7789.BLACK)
    
    
    display.draw(font, "Auj.",x,y)
    display_meteo(display,font,infos_meteo["current"],x, y+20) #x,y+20)
    
    display.draw(font, "Dem.", x, y+40) #x+50, y)
    display_meteo(display,font,infos_meteo["demain"], x, y+60) #x+50,y+20)
    
    display.draw(font, "Apr-Dem.", x, y+80) #x+90, y)
    display_meteo(display,font,infos_meteo["apres-demain"], x, y+100) #x+90,y+20)

def main_menu(display,font,infos_temp,infos_meteo):
    display_time(display,font, 10, 111) # x = 52 pour un semblant de centrage
    display_temperature(display,font,infos_temp,10,151)
    
    if infos_meteo["current"]["precipitation"][0] > 0:
        icone = "pluie"
        motor.baissebg()
        motor.levebd()
    else:
        icone = "soleil"
        motor.baissebd()
        motor.levebg()
    
    meteo_widget(display,font,infos_meteo,10,191)
    meteo_icon(display,icone,110,111)

def animation(display,name:str,iteration:int,i_fixe:int): # list_anim : {"content":(2,1),"endormi":(4,1),"somnole":(3,1),"observe":(0,0),"monocle":(2,1)}
    display.fill_rect(0,0,170,105,st7789.BLACK)
    for i in range(2,iteration+1):
        display.png("/images/" + name + "/" + name + str(i) + ".png",0,0)
        if name == "endormi":
            sleep(0.3)
    for i in range(iteration,0,-1):
        display.png("/images/" + name + "/" + name + str(i) + ".png",0,0)
        if name == "endormi":
            sleep(0.3)
    display.png("/images/" + name + "/" + name + str(i_fixe) + ".png",0,0)
    

Pour finir nous avons le fichier api_txt.py qui s’occupe de gérer des fichiers de textes importants pour le fonctionnement du programme, présents dans le dossier data.

"""
api[0] => Google Maps API
api[1] => Météo API
"""

def get_api_counter():
    fichier_r = open("data/counter-api.txt",'r')
    liste = fichier_r.read().split(",")
    fichier_r.close()
    return liste

def add_api_counter(indice_api): # ne fonctionne qu'avec deux apis
    fichier_w = open("data/counter-api.txt",'w')
    val = get_api_counter()
    val[indice_api] = int(val[indice_api]) + 1
    
    fichier_w.write(str(val[0]) + "," + str(val[1]))
    fichier_w.close()

def set_api_counter(indice_api,value): # ne fonctionne qu'avec deux apis
    fichier_w = open("data/counter-api.txt",'w')
    api_value = get_api_counter()
    api_value[indice_api] = str(value)
    fichier_w.write(api_value[0] + "," + api_value[1])
    fichier_w.close()

Maintenant que toutes nos briques sont construites, il ne reste qu’à écrire le fichier python principal qui va organiser tout le programme tel un chef d’orchestre.

Assemblage final

Maintenant, il faut simplement assembler notre montage électronique à notre construction LEGO. Puis à brancher notre ESP32 à un câble, lui injecter le code, et tout en le gardant brancher, le lancer, et la magie opère !

Fichiers

Veuillez noter que vous devrez ajouter votre propre clé api Google Maps API pour que la position soit mise à jour (dans boot.py à la ligne 21). Sinon vous pouvez changer manuellement la position dans le fichier old-location.txt se trouvant dans le dossier data. Veuillez respecter l’ordre suivant et ne surtout rien ajouter d’autre dans le fichier, pas même un espace :

latitude,longitude

Vous devrez également ajouter vos propres informations pour un point d’accès dans le fichier boot.py au niveau de la ligne 19 : wlan_info = ("SSID","MDP")
Le projet ne fonctionne pas sans une connexion à internet !!

Projets

Astronomie : Planète face au soleil

L’astronomie fait partie intégrante de notre histoire en tant qu’être humain. On peut la percevoir comme une fenêtre ouverte sur l’immensité du cosmos. Chaque découverte, chaque image semble être ajoutée au puzzle infiniment grand et complexe qu’est l’univers.

Mise en avant de notre idée de projet

Tout d’abord, nous avons beaucoup réfléchi à la base de notre projet. Nous avons décidé de choisir l’astronomie, un domaine vaste et varié qui offre de nombreuses options. Nous avons opté pour une perspective de l’espace, imaginant une planète face au soleil avec sa « lune ». Notre inspiration est donc venue de la Terre.

Le fond

Dans un premier temps, concernant le fond, nous avons dû commencer obligatoirement par nous plonger dans l’ambiance de l’espace. Ainsi, nous avons choisi un fond noir. Pour parvenir à cela, nous nous sommes inspirés d’autres codes de création et en avons tiré ceci :

pensize(10000)
goto(100,1)
pensize(1)

Les étoiles

Après avoir ajouté le fond, il fallut combler ce vide. Quoi de mieux que des centaines d’étoiles ? Nous avons utilisé un script permettant de placer aléatoirement 400 étoiles de couleur blanche, de taille plutôt petite et identiques les unes aux autres, sur l’image avec ce script :

def etoile(longueur, x, y, couleur=(1, 1, 1)):
    penup()
    goto(x, y)
    pendown()
    pencolor("white")
    fillcolor("white")

for i in range(5):
        forward(longueur)
        right(144)
        forward(longueur)
        left(72)
        
        
for y in range(400):
    x, y = randint(-800, 800), randint(-400, 400)
    longueur = 2
    couleur = (randint(247, 255), randint(127, 255), randint(0, 12))
    etoile(longueur, x, y, couleur)

Et voici le résultat des étoiles sur le fond noir :

Le soleil

Après avoir ajouté les étoiles, nous souhaitions inclure un élément principal à l’image : le soleil, élément majeur de notre existence et de celle de notre planète. Nous l’avons positionné en haut à gauche de l’image pour montrer son importance cruciale. Pour ce faire, nous avons ajouté un cercle à l’aide d’une fonction que nous avons nommée « planete », puis nous lui avons attribué des coordonnées, une couleur et une taille.

# Fonction de cercle
def planete(rayon, x, y, couleur):
    penup()
    goto(x, y)
    pendown()
    pencolor(couleur)
    fillcolor(couleur)
    begin_fill()
    circle(rayon)
    end_fill()    
    

# Dessiner le soleil
rayon_planete = 300  
x_planete, y_planete = -450, 30  
couleur_planete = "#FFC300"
planete(rayon_planete, x_planete, y_planete, couleur_planete)

Puis, nous avons souhaité intégrer une sorte de dégradé de couleur pour apporter réalisme et nuance. Après des recherches, nous avons finalement ajouté un cercle de couleur différente, de taille plus petite, au même emplacement que le soleil avec ce code :

# Dessiner dégradé
rayon_planete = 240
x_planete, y_planete = -450, 90
couleur_planete = "orange"
planete(rayon_planete, x_planete, y_planete, couleur_planete)

Et voici le résultat du soleil :

La planète

Après tout cela, l’image restait incomplète et semblait toujours trop vide. Nous avons donc ajouté une planète que nous avons placée à l’opposé du soleil pour donner l’impression qu’elle est en face de celui-ci. Nous avons décidé de représenter une planète bleue en référence à la Terre en utilisant la même fonction à celle utilisée précédemment pour dessiner un cercle. Voici le code utilisé :

# Dessiner Planete 
rayon_planete = 300
x_planete, y_planete = 500, -600
couleur_planete = "blue"
planete(rayon_planete, x_planete, y_planete, couleur_planete)

Et voici le résultat en rajoutant la planète :

La lune

Pour finaliser notre image, nous avons pensé qu’il manquait quelques détails à ajouter. Quoi de mieux que d’inclure une lune, que nous avons positionnée près de la planète bleue, pour accroître le réalisme. Pour créer cette lune, nous avons réutilisé la fonction des cercles. Nous avons défini sa couleur en gris et l’avons rendue plus petite, car elle est moins importante que le soleil et la planète. Ensuite, à l’intérieur de cette lune, nous avons ajouté deux cercles encore plus petits pour représenter des cratères, de couleur plus foncée pour les faire ressortir. Voici le code utilisé :

# Dessiner Lune
rayon_planete = 100
x_planete, y_planete = 370, 40
couleur_planete = "#d4d3d0"
planete(rayon_planete, x_planete, y_planete, couleur_planete)

# Dessiner cratère
rayon_planete = 20
x_planete, y_planete = 370, 80
couleur_planete = "#b4b1a8"
planete(rayon_planete, x_planete, y_planete, couleur_planete)
rayon_planete = 30
x_planete, y_planete = 320, 145
couleur_planete = "#b4b1a8"
planete(rayon_planete, x_planete, y_planete, couleur_planete)

Et voici le résultat finale :

Télécharger le .py

Projets

Cinéma: Matrix et le binaire

« Are you telling me I can dodge bullets ? » Neo, The matrix. Cette citation représente la base de notre projet dans lequel est présenté le personnage Neo au visage hélicoïdal stoppant les balles sur un fond de chiffres binaires aléatoirement positionnés.

I – Le fond

Premièrement, le fond nous semblait être la partie la plus importante du projet. Nous avons donc commencé par poser un fond noir avec les lignes de code suivantes

color('black')
pensize(2000) #peu conventionnel 😅
goto(-635, 350)
forward(700)

Après cela, nous y avons superposé les caractères 1 et 0 que nous avons définis avec le code suivant.

def un():
    pensize(2)
    color('green')
    setheading(60)
    forward(12)
    setheading(270)
    forward(20)

def zero():
    setheading(90)
    circle(8)

Nous avons ensuite créé une suite de boucle pour placer aléatoirement des 1 et des 0 en colonnes. Pour cela, nous avons utilisé le module « random » avec la fonction « random.choice » pour sélectionner aléatoirement un nombre dans une liste prédéfinie, ensuite, une condition nous a permis de placer les chiffres. Pour ordonner ces derniers, une incrémentation aux coordonnées déjà inscrites (définissant la position du premier caractère posé) à la fin de chaque boucle a été nécessaire. Voici le code

y = 330
x = -620
    
for i in range(50):
    penup()
    goto(x, y)
    pendown()
    
    for i in range(20):
        list1 = [1,2]

        if random.choice(list1) == 1: 
            un()
        else:
            zero()
            
        penup()
        goto(x, y - 40 * (i + 1))
        pendown()
        
    x += 25 #incrémentation de la distance entre chaque colonnes (horizontalement)
    

Voici le résultat du fond finalisé :

II – Le personnage, Neo

1 – La main

La main était aussi une partie importante de l’image : elle constitue le point de fuite centrale de l’image, où se dirige toutes les balles. Voici la main, constituée de lignes plus ou moins courtes (doigts) et d’un cercle (paume).

2 – Le corps

Nous avons donc créé le corps d’un homme bâton, avec de simples lignes et un cercle pour la tête. Voici le code :

def corps():
    pensize(5)
    color('white')
    penup()
    goto(0,-70)
    pendown()
    setheading(90)
    left(110)
    forward(180)
    left(25)
    forward(80)
    left(180)
    forward(80)


    setheading(90)
    forward(40)
    right(180)
    forward(150)
    setheading(90)
    right(150)
    forward(90)
    left(180)
    forward(90)
    setheading(90)
    left(150)
    forward(90)
    right(180)
    forward(90)
    setheading(90)
    forward(150)

    right(90)
    circle(50)
    penup()
    setheading(90)
    forward(50)
    pendown()
corp()

Voici le rendu :

3 – La spirale

Pour créer cette spirale, il n’a suffit que de 2 lignes, avec une boucle for :

for i in range(48):
    circle(i,20) # i est donc augmenté a chaque répétition 

Le rendu fut le suivant :

III – Les balles

Pour créer les balles, nous avons créé une fonction, en utilisant les méthodes « begin_fill » et « end_fill » permettant de remplir tout ce que l’on dessine en trait. Voici le code :

def balle():
    pensize(3)
    color('#A9A9A9')
    begin_fill()
    forward(30)
    left(143)
    forward(27)
    left(110)
    forward(15)
    end_fill()
    left(180)
    forward(7)
    color("grey")
    begin_fill()
    circle(10)
    end_fill()

Voici le résultat

Pour créer plus de dynamisme, nous avons opté pour des tirets, encore avec une fonction, appelées (comme les balles) plusieurs fois. Nous avons donc créé une boucle for, se répétant 18 fois :

def tiret():
    for i in range(18):
        pensize(7)
        color("white")
        forward(5)
        penup()
        forward(15)
        pendown()

Pour une balle, nous avons combiné les deux fonctions en les appelant de la sorte :

penup()
left(50)
goto(300,-150) 
pendown()
balle()
penup()
left(90)
forward(30)
pendown()
tiret() 
right(90)

Nous avons répété ce pattern sept fois, en changeant leurs positions (goto).

IV – L’image finale

V – Les problèmes rencontrés

Le seul ‘problème’ qu’on a rencontré fut pour le fond, pour utiliser le module random dans une liste, mais nous avons relu nos exercices sur ce thème et avons facilement trouvé la solution.

VII – Télécharger le .py

Nous vous mettons au défi d’optimiser ce code au maximum, sans changer le rendu.