Pure Basic
Comment ? Du BASIC ? Ici ? Et bien oui car, comme disait quelqu'un dont je ne me rappelle pas le nom, ce n'est pas le langage qui est important mais la façon de l'utiliser pour résoudre un problème. Et c'est vrai que lorsque l'on voit la profusion de langages modernes pour la programmation on ne peut être que surpris de parler BASIC.
Nous aurions pu effectivement parler d'un langage utilisant le paradygme de la programmation orientée objet tel que C++ ou C# ou encore Java mais nous allons nous concentrer sur un BASIC qui n'a de basique que le nom. Bien qu'étant un langage procédural (pourvu qu'il le reste), PureBasic n'en reste pas moins un langage moderne multiplateforme, simple d'accès et très puissant. En effet, contrairement aux différents BASIC que nous avons connus, qui souffraient de beaucoup de lenteurs et de programmation "spaghetti", PureBasic est un BASIC compilé. Cela lui donne une grande vélocité dans les différents traitements, largement suffisant pour la majorité des applications que vous aurez envie de programmer.
Au contraire de nombre de langages disponibles, PureBasic est un ensemble de développement qui n'est pas gratuit. L'investissement en vaut la chandelle car la licence est acquise définitivement et vous donne accès à toutes les mises à jours disponibles, aussi bien mineures que majeures. Et puis cela permettra à un groupe de programmeurs talentueux de continuer à travailler pour nous apporter de plus en plus de fonctionnalités et de performances. Good Job Guys !
Il suffit simplement de télécharger et installer l'exécutable depuis le site de l'éditeur https://www.purebasic.com/french/. Le programme est disponible en version 32 et 64 bits en fonction de l'OS que vous utilisez. Vous obtenez alors un Environnement de Développement Intégré (IDE) qui vous permettra de produire du code et de générer vos interfaces graphiques (GUI). C'est rapide à installer et l'éditeur fourni est suffisant pour vos réalisations, incorporant de la coloration syntaxique et une complétion de code.
L'IDE de PureBasic incorpore un système d'aide efficace avec un nombre d'exemples importants. Pour les appels aux différentes API, vous pouvez rajouter de l'aide externe. Sous Windows, vous pouvez ajouter les fichiers HLP relatifs aux APIs de base, ainsi que celles couvrant OpenGL, MAPI, les sockets et autres en créant un dossier Help directement dans le dossier de PureBasic. De même pour l'assembleur en ajoutant le fichier ASM.HLP, toujours dans le même dossier Help du dossier de PureBasic. Par la suite, en appuyant sur la touche F1 lorsque le curseur d'édition est sur un nom de procédure, vous devriez obtenir une fenêtre avec l'aide en ligne. Si jamais la touche F1 n'est pas opérationnelle, consultez l'aide depuis le menu Help / External Help.
Depuis la création du premier langage BASIC, nombre d'évolutions ont été apportées à ce langage. PureBasic apporte également ses propres améloirations pour en faire :
PureBasic permet de réaliser des programmes pour la console (DOS, Shell, Terminal...) ou bien en mode graphique en incorporant une interface graphique (GUI). La partie graphique sera traitée ultérieurement alors intéressons-nous à la version console.
Afin de réaliser un exemple rapide, voici un simple Hello World comme nous avons l'habitude d'en voir fleurir avec tous les langages.
OpenConsole()
PrintN("Hello World of PureBasic...")
Repeat
Until Inkey() <> ""
CloseConsole()
Le code ci-dessus demande à PureBasic d'ouvrir une fenêtre en mode console et d'afficher simplement une chaîne de caractères. Le programme attend ensuite qu'une touche soit pressée pour se terminer. La console est alors fermée. Par défaut, le console ne permet d'afficher que des caractères textuels. La commande EnableGraphicalConsole() permet de basculer en mode graphique et ainsi de pouvoir utiliser le positionnement de caractères dans la console et de la couleur pour les caractères.
If OpenConsole()
EnableGraphicalConsole(1)
ConsoleLocate(7, 8)
PrintN("Appuyez sur [Entree] pour quitter")
Input()
EndIf
En PureBasic il n'est nul besoin de déclarer les variables que nous allons utiliser, hormis pour les structures et les tableaux. Toutefois, celles-ci peuvent contenir des types de valeurs différentes et PureBasic impose un certain typage. Le tableau ci-après donne les différents types disponibles.
Extension | Description |
---|---|
.q | Un nombre signé exprimé sur 64 bits (quad). |
.i | Un nombre signé entier exprimé en 32 ou 64 bits selon la plateforme (int). Type à privilégier pour la majorité des variables, sauf octet et flottant. |
.l | Un nombre entier long signé exprimé sur 32 bits (long). |
.w | Un mot signé exprimé sur 16 bits (word), soit des valeurs de -32768 à 32767. |
.u | Un nombre non signé de 16 bits (word), soit des valeurs de 0 à 65535. |
.b | Un nombre signé exprimé sur 8 bits (byte), soit des valeurs de -128 à 127. |
.a | Un nombre non signé exprimé sur 8 bits (byte), soit des valeurs de 0 à 255. Type à privilégier pour les accès mémoire car la mémoire fonctionne avec des octets. |
.s | Une chaîne de caractères |
var$ | Une chaîne de caractères définie sous un autre format. |
var${x} | Une chaîne de caractères de longueur fixe. |
.c | Un seul caractère, entre '', exprimé sur un ou deux octets selon l'usage ou non de l'Unicode. |
*p = @v | Déclaration d'un pointeur sur une zone mémoire. Le pointeur peut être soit en 32 ou 64 bits selon le type de la variable et la plateforme. |
.f | Un nombre en virgule flottante |
.d | Un nombre double en virgule flottante. Type à privilégier pour les valeurs numériques à virgule. |
var.structure | Déclaration d'une variable de type Structure |
OpenConsole() ; ouverture d'une console pour afficher des informations
b.b = 247 ; déclaration d'un entier sur 8 bits signé
PrintN(Str(b)) ; retourne un octet signé soit -9
PrintN(StrU(b, #PB_Byte)) ; Affiche une valeur non signée sur un octet soit 247
a.a = 247 ; déclaration d'un octet sur 8 bits non signé
PrintN(Str(a)) ; affiche 247
PrintN(StrU(a)) ; affiche 247
; Autre façon d'accéder à la variable
x.a = 247
PrintN(Str(PeekB(@x))) ; affiche un octet signé soit -9
PrintN(Str(PeekA(@x))) ; affiche un octet non signé soit 247
Repeat
Delay(1) ; laisse du temps aux autres processus systèmes
Until Inkey() <> "" ; attend la frappe d'une touche quelconque
CloseConsole() ; Fermeture de la console
Un octet occupe 8 bits en mémoire. En représentation binaire 247 = %11110111. Le signe est déterminé
par la valeur du bit de poids fort (le plus à gauche) soit ici 1. En représentation décimale nous avons
alors %(1)1110111 => complément à 2 => %(1)0001001 = -9. Pour plus d'informations sur le binaire, voir
la page sur les systèmes numériques www.gerbelotbarillon.com/sysnum.html.
Pour éviter les erreurs de compilation ou les malentendus au sujet de noms de variables, PureBasic met à notre disposition la commande EnableExplicit qui va signaler au compilateur de PureBasic de ne pas accepter de variable si elle n'a pas été définie précédemment. Ainsi le code suivant va fonctionner mais ne va pas donner le résultat attendu
a = 1
a = aa + 1
Debug a ; va donner 1 au lieu de 2 qui est la valeur attendue
Dans le code ci-dessus, la valeur attendue est 2 mais le compilateur va renvoyer simplement 1 car pour
lui la variable aa n'est pas une erreur et va la considérer comme égale à 0 lors de son initialisation.
Pour éviter ce genre de bévue, on va régler le problème en utilisant EnableExplicit et en déclarant les
variables avant leur utilisation.
EnableExplicit
Define a.i ; EnableExplicit nous force à déclarer les variables avant usage
a = 1
a = aa + 1 ; Cette ligne génère une erreur à la compilation car aa n'est pas déclarée avant son utilisation
Si on récapitule :
Les expressions sont normalement évaluées de gauche à droite, à savoir que la partie à gauche du symbole '=' récupère la valeur qui est à droite de ce même symbole. Cette valeur à droite peut être un nombre direct, une variable ou bien une opération complexe faisant intervenir une combinaison de variables.
Les expressions doivent rester simples dans leur expression. Il ne faut pas confondre PureBasic et le C
tout de même. On ne peut donc pas avoir quelquechose du genre b = 1 : a = (a = b) > c
...
Les chaînes de caractères sont de longueur variables par défaut. La fin de ces chaînes est marquée par un caractère 0. Un caractère occupe 1 octet (2 en mode Unicode).
ATTENTION : pour ceux qui proviennent d'un langage bas niveau type C, l'utilisation de chaînes de caractères en tant que buffer lors de la lecture de contenu binaire est déconseillée car un caractère zéro est si vite arrivé...et le buffer si vite corrompu... Il est alors recommandé d'utiliser plutôt un buffer de type chaîne de caractères de taille fixe. Pour ceux qui ont déjà oublié, ce type de variable se note ${x} ou .s{x}.
a.s = "Hello world" ; taille variable, implicitement terminée par un caractère NULL
b.s{11} = "Hello world" ; taille fixe, caractère NULL autorisé
Les chaînes de taille fixe sont très utilisées dans les structures de données ainsi que pour le passage de paramètre aux différentes API gérées par les bibliothèques externes à PureBasic.
Il est possible de convertir des nombres en chaînes de caractères et l'inverse en utilisant les couples de fonctions Val() et Str(). Val() convertit une chaîne de caractères représentant un nombre en ce nombre alors que Str() convertit un nombre en sa représentation sous forme de chaîne de caractères. Ces fonctions de conversion acceptent des déclinaisons sur des types de variables définis comme StrD() pour les conversions de nombres entiers double longueur, StrF() pour la conversion de nombres en virgule flottante ou StrU() pour les nombres non signés. Idem pour Val() avec ValF() pour convertir une chaîne de caractères représentant un nombre en virgule flottante en nombre à virgule flottante ou ValD() pour l'équivalent en nombre entier format double longueur. Il est bien sûr possible de traiter des valeurs exprimées en binaire (avec % devant) ou hexadécimal (avec un $ devant).
Nous pouvons utiliser un certain nombre de fonctions pour manipuler les chaînes de caractères afin d'extraire certains caractères, ou bien an concaténant les chaînes les unes avec les autres, pour créer des chaînes vides...
s1.s = "12"
s2.s = "345678"
s.s = s1 + s2 ; concaténation des deux chaînes de caractères
Debug s ; Affiche "12345678"
Debug Len(s) ; 8
Debug Left(s, 4) ; affiche les 4 premiers caractères de la chaîne soit "1234"
Debug Right(s, 4) ; affiche les 4 derniers caractères de la chaîne soit "5678"
Debug Mid(s, 4, 2) ; affiche 2 caractères à partir de la position 4 de la chaîne, soit "45"
Debug Space(10) ; chaîne " " 10 caractères espace
Debug Trim(" AZERTY ") ; affiche "AZERTY"
Debug UCase("Hello") ; affiche "HELLO"
Debug ReplaceString(h, "45", "66") ; affiche "366678"
Debug FindString(s2, "45", 1) ; affiche la position en commençant à 1 à laquelle la chaîne est trouvée
; Le code suivant va afficher chaque mot de la chaîne séparé par le séparateur " "
For k=1 To 7
Debug StringField("Je suis une chaîne contenant des champs", k, " ")
Next
Pour produire un affichage propre des informations, nous pouvons formatter la sortie de ces chaînes par les commandes LSet() et RSet().
Debug RSet("L", 4) ; affiche " L" soit une chaîne de 4 caractères avec des espaces pour compléter
Debug RSet("L", 4, "*") ; affiche "***L"
Debug LSet("L", 4) ; affiche "L "
Debug LSet("L", 4, "*") ; affiche "L***"
Les variables booléennes permettent de tester si les informations sont vraies ou fausses. Les valeurs sont #True pour Vrai et #False pour Faux. Ce sont des ajouts des versions 5 et ultérieures que vous pouvez mettre à la place des simples tests =0 pour Faux et <>0 pour Vrai. Il est donc possible aujourd'hui de faire
a = Bool(b <> c)
au lieu de ceci
a = #False
If b <> c
a = #True
Endif
Si la condition est vraie alors on exécute un morceau du programme, sinon on exécute un autre morceau de programme.
a.l = 10
If a = 10
Debug "a vaut 10"
ElseIf a = 20
Debug "a vaut 20"
Else
Debug "a vaut une autre valeur"
Endif
L'utilisation de Select permet de tester une variable contre un grand ensemble de valeurs possibles. Cela permet d'éviter d'utiliser une multitude de If / ElseIf et de simplifier la lecture du programme. Contrairement à d'autres langages pour lesquels la construction select / Case existe (un bon vieux switch des familles), en PureBasic il est possible de tester des valeurs numériques, des chaînes de caractères et même des intervalles de données pour une variable.
a = 5
Select a
Case 1
Debug "un"
Case 2,3
Debug "deux ou trois"
Case 4 To 10
Debug "dans l'intervalle 4 à 10"
Default
Debug "valeur inconnue"
EndSelect
Il n'est par contre pas possible de mixer dans un Select / Case des comparaisons sur une chaîne de caractères
en même temps que des valeurs numériques. Eh oui ! On compare bien sûr selon le type de la variable testée...
Et pour les valeurs flottantes ? Et bien PureBasic va d'abord arrondir à la valeur inférieure avant de procéder
à la comparaison.
Pour répéter des opérations il existe 3 types de boucles :
; Gestion des boucles
; Avec For / Next
For n = 1 To 10
Debug n
Next
; avec Repeat / Until
n = 1
Repeat
Debug n
n + 1
Until n > 10
; avec While / Wend
n = 1
While n <= 10
Debug n
n + 1
Wend
Avec un peu de structure dans le code, on s'aperçoit vite que la seule vraie gestion de boucles est
réalisée avec un While / Wend bien calibré.
L'exception dans la gestion des boucles est la commande ForEach qui sera quasiment exclusivement utilisée pour la gestion des listes chaînées. Nous verrons dans un chapitre ultérieur de quelle manière.
Parfois il est nécessaire de pouvoir interrompre une boucle, même si normalement, si le code est bien conçu, vous ne devriez pas avoir à le faire. Il existe cependant deux instructions :
Les structures de données sont les meilleurs moyens de gérer et manipuler des ensembles d'informations. Ces structures peuvent contenir tous types de variables.
; Structures
; Tous les exemples sont donnés pour une configuration en mode non-Unicode (soit un caractère sur un octet)
; Avec une chaîne de caractères
Structure tree
a.l
b.s
EndStructure
apple.tree ; variable de type 'tree' (une structure de données)
apple\a = 1
apple\b = "Apple"
Debug(apple\a)
Debug(apple\b)
; Avec un tableau d'octets
Structure tree_octets
a.l
b.b[16]
EndStructure
apple2.tree_octets
apple2\a = 2
PokeS(@apple2\b, "Apple 2") ; écrit 8 caractères dans la zone mémoire (7 octets + 1 caractère de fin de chaîne)
Debug(apple2\a)
Debug(apple2\b) ; n'affiche que le premier caractère du tableau
Debug(PeekS(@apple2\b)) ; affiche toute la chaîne à l'adresse apple2\b
Debug("")
For n = 0 To 7
Debug(Hex(PeekA(@apple2\b+n))) ; affichage du code Ascii du caractère en RAM
Next
; Avec une chaîne de longueur fixe
Structure tree_fixed
a.l
b.s{10}
EndStructure
apple3.tree_fixed
apple3\a = 3
apple3\b = "Apple3"
Debug ""
Debug apple3\b
PokeA(@apple3\b+5, '4') ; Modification du 6eme caractère de la chaîne
Debug apple3\b
apple3\b = "PureBasic is my name" ; la chaîne dépasse le buffer mais ne génère pas d'erreur
Debug apple3\b ; affiche les 10 caractères du buffer défini dans la structure
Historiquement les langages de programmation ont toujours fait une différence entre fonctions et procédures. Les fonctions sont des procédures qui retournent un résultat. PureBasic a simplifié en stipulant qu'une procédure pouvait également renvoyer un résultat à la fin du traitement. Une procédure est un élément clé dans un programme car cela permet d'effectuer un ensemble d'actions plusieurs fois sans dupliquer le code requis pour ces actions. Comme tout objet structuré de PureBasic, la définition d'une procédure se termine par la commande EndProcedure.
Procedure proc(a.l, b.l, c.d)
Debug a
c = a + b
ProcedureReturn c
EndProcedure
Le morceau de code ci-dessus montre la déclaration d'une procédure avec 3 paramètres en entrée. La valeur
de retour est transmise par la commande ProcedureReturn. Cette commande peut être placée n'importe
où dans le corps de la procédure. Elle est soit :
Les procédures peuvent prendre des paramètres en entrée, en nombre fixe ou bien avec des paramètres optionnels disposant de valeurs par défaut. Ainsi les appels à la procédure peuvent prendre un nombre variable de paramètres :
Procedure procvar(a.l, b.d, c.l=42)
Debug a
Debug b
Debug c
EndProcedure
; ...
procvar(1, 2.0) ; appel avec deux paramètres
procvar(3, 4.0, 33) ; appel avec trois paramètres
On désigne par portée d'une variable l'emplacement du programme dans lequel on peut utiliser une variable à partir de l'endroit où celle-ci a été déclarée. On distingue la portée locale et la portée globale.
Global c.l ; variable déclarée globale pour tout le programme
Procedure proc()
a.l = 42
b.l = 33
c.l = 25
EndProcedure
; Début du programme principal
a.l = 1
b.l = 2
c.l = 3
proc()
Debug a ; affiche 1
Debug b ; affiche 2
Debug c ; affiche 25
On déclare une variable 'c' comme globale à tout le programme. Suite à la définition des variables
'a' et 'b' (considérées comme 'locales' au programme) et à l'affectation d'une valeur à chacune d'elles,
la variable 'c' précédemment déclarée comme globale se voit également affectée d'une valeur.
Ensuite nous faisons un appel à la procédure proc() qui va modifier les valeurs de a, b et c. Nous
affichons ensuite les valeurs résultantes pour s'apercevoir que :
Il se pourrait que vous ayez besoin de déclarer une variable locale à une procédure de même nom qu'une variable globale. Cela peut arriver oui oui. Pour cela, il va falloir utiliser la commande Protected.
Global a
a = 10
Procedure Change()
Protected a
a = 20
EndProcedure
Debug a ; Affichera 10 car la variable a été protégée.
La dernière façon de déclarer une variable est d'utiliser le mot-clé Static. Une variable déclarée comme statique a priorité sur les variables globales et n'est pas détruite entre chaque appel à la procédure. Une variable globale qui porterait le même nom qu'une variable de procédure déclarée comme statique ne serait pas prise en compte.
Global a
a = 10
Procedure Change()
Static a
a+1
Debug "Dans la Procédure: "+Str(a) ; Affichera 1, 2, 3 car la variable s'incrémente à chaque appel de la procédure.
EndProcedure
Change()
Change()
Change()
Debug a ; Affichera 10, car une variable 'static' n'affecte pas une variable 'global'.
En fait il existe d'autres types de portées pour les variables :
Un nombre important de fonctions des API de programmation (notamment celles de Windows) ont été intégrées dans PureBasic. La convention d'appel de ces fonctions reste la même que celle sous un autre langage à la différence qu'il faut rajouter un underscore ('_') à la fin du nom de la fonction. Si PureBasic ne reconnaît pas cette fonction, il vous faudra ouvrir la DLL la contenant par un appel à OpenLibrary(). Pour les passages des paramètres aux fonctions, il n'est pas nécessaire de se préoccuper de la longueur de l'entier car PureBasic s'adapte. Il en est de même pour les valeurs de retour, sauf que pour le coup elles doivent tout de même pouvoir contenir la valeur retournée...
Une DLL non reconnue directement par PureBasic va devoir être implicitement ouverte et donc vous allez devoir récupérer un handle sur cette bibliothèque avant de pouvoir manipuler les fonctions qu'elle contient.
wsa.WSADATA ; prépare la structure de gestion des sockets Windows
; Ouverture de la bibliothèque par OpenLibrary(). Cet appel retourne le résultat dans winsock_result
; et le handle de la DLL dans le premier paramètre nommé winsock_handle.
winsock_result.i = OpenLibrary(winsock_handle, "WSOCK32.DLL")
; Obtient l'adresse de la fonction depuis la DLL
*wsa_startup = GetFunction(winsock_handle, "WSAStartup")
; Appel de la fonction de la DLL par son adresse avec remplissage des différents paramètres
wsa_retval.i = CallFunctionFast(*wsa_startup, $101, @wsa)
CloseLibrary(winsock_handle) ; fermeture de la bibliothèque DLL
Cet exemple de code illustre l'approche de l'accès à une DLL. Il est important de ne pas oublier de
refermer la bibliothèque avant la fin du programme sous peine de fuite mémoire ou blocage de DLL.
Une autre façon de faire serait d'utiliser l'appel à CallFunction() ou bien de faire usage des pointeurs
et des prototypes...
Programmer réserve toujours la surprise d'obtenir des erreurs à l'usage de notre nouveau programme. PureBasic montre qu'il joue dans la cour des grands en mettant à disposition un débuggeur. Cet outil va permettre d'afficher dans une fenêtre indépendante tous les messages et valeurs que vous désirez à différentes étapes de votre programme. Si vous utilisez le débuggeur veillez à ce qu'il soit activé dans l'IDE sinon le code suivant l'instruction Debug() ne sera pas exécuté...
a = 1
Debug a ; affiche le contenu de la valeur a
Même si PureBasic se veut simple, cela ne signifie pas simpliste. Il est en effet doté de la possibilité de gérer des structures de données complexes, telles que définies par le couple Structure / EndStructure qui peuvent mêler plusieurs variables de différents types sous une seule nomenclature. Mais PureBasic peut également gérer des tableaux, des listes et des tableaux associatifs. Ces trois derniers types permettant de manipuler des collections d'objets de même type (type simple ou structure).
Pour passer une liste en paramètre d'une procédure il suffit de faire précéder le nom de la variable du mot-clé List. La façon de faire est identique pour les Tableaux (avec le mot-clé Array) et pour les tableaux associatifs (avec le mot-clé Map).
Comment les tableaux peuvent être multi dimensionnels (non ce n'est pas de la science-fiction ^_^), il faut également spécifier le nombre de dimensions de l'objet en tant que paramètre de la procédure.
Soyez toutefois rassuré que ce n'est pas l'intégralité de la liste ou du tableau que nous passons en paramètre mais juste le pointeur sur le début de l'objet. C'est ce que l'on nomme le passage par référence, à l'opposé du passage par valeur, qui est le mode classique de gestion des paramètres des procédures où ce sont des copies des valeurs des variables qui sont transmises aux variables locales de la procédure.
Procedure change_liste(List y.l())
SelectElement(y(), 0) ; sélectionne le premier slot de la liste
y() = 2 ; le premier élément contient maintenant la valeur 2
EndProcedure
NewList x.l() ; création d'une liste d'entiers longs
AddElement(x()) ; ajoute un slot vide dans la liste
x() = 1
change_liste(x()) ; appelle la fonction avec la référence de la liste en paramètre
SelectElement(x(), 0) ; reprend le premier élément de la liste
PrintN("Liste : " + Str(x())) ; affiche le contenu du premier élément de la liste
Procedure change_array(Array q.l(1)) ; le 1 signale la dimension du tableau (ici tableau monodimensionnel)
q(1) = 42 ; change la valeur de la case 1 du tableau
EndProcedure
Dim p.l(10) ; tableau de 11 entiers longs
p(1) = 1 ; affecte la valeur 1 à la première case du tableau
change_array(p())
PrintN("Tableau : " + Str(p(1))) ; affiche la nouvelle valeur du tableau
Procedure change_map(Map m.s())
m("1") = "42"
EndProcedure
NewMap n.s() ; création d'un tableau associatif
n("1") = "1" ; affecte "1" au champ "1" du tableau associatif
change_map(n())
PrintN("Map : " + n("1"))
Une constante est une variable qui porte une valeur qui ne change pas... Vous n'avez pas besoin de déclarer le type de la constante, juste de l'affecter.
a = #True ; par défaut = 1
b = #False ; par défaut = 0
La version un peu plus utile d'une constante est lorsqu'elle est utilisée conjointement à une énumération. C'est une structure de données qui permet de regrouper des ensembles de valeurs constantes afin de simplifier la programmation et la lecture du programme lui-même, de passer des valeurs à des bibliothèques externes...
Enumeration
#Valeur1
#Valeur2
#Valeur3
EndEnumeration
Debug #Valeur1 ; Affiche 0
Debug #Valeur3 ; Affiche 2
Ainsi l'usage montre que les énumérations commencent toujours à la valeur 0 par défaut et s'incrémentent
de 1 unité à chaque nouvelle valeur. Il est bien évidemment possible de modifier cela en affectant une
valeur à une variable. Les suivantes continueront alors en séquence à partir de la valeur précédente.
Enumeration
#valeur10
#valeur20 = 20
#valeur30
EndEnumeration
Debug #valeur10 ; 0
Debug #valeur20 ; 20
Debug #valeur30 ; 21
On voit bien que l'affectation d'une valeur numérique à une constante modifie la séquence pour les élements
suivants de l'énumération.
Nous attaquons une notion plus subtile de la manipulation des espaces mémoire avec les commandes Peek() et Poke() (nostalgie du C64 tout d'un coup non ?). Lors de l'exécution d'un programme, il va occuper une certaine quantité de mémoire de votre ordinateur. De même pour les diverses variables que vous allez utiliser, chacune occupant un volume différent en fonction de son type.
Les commandes Peek() et Poke() font exactement l'opposé l'une de l'autre : Peek va permettre de lire un espace mémoire alors que Poke va permettre d'écrire dans cet espace. L'une est presque sûre, l'autre est plutôt à utiliser avec précaution sous peine de plantage direct du programme.
PureBasic utilise par défaut la notion d'Unicode pour la gestion de ses caractères. Ceux-ci occupent alors 2 octets en mémoire. Vous pouvez changer ce comportement en configurant l'IDE PureBasic pour ne pas passer en mode Unicode (Menu Compilateur -> Options du compilateur). La fonction StringByteLength(chaine$) peut vous renseigner sur cela également car elle vous retourne le nombre d'octets occupés par votre chaîne.
L'autre point à se rappeler est que les chaînes se terminent toujours par un caractère '0', même si vous ne le voyez pas. Ainsi "1234" n'occupera pas 4 octets (ou 8 en Unicode) mais plutôt 5 (ou 10 en Unicode) car il faut compter le zéro terminal. C'est une notion importante lors de l'usage de Poke() car vous pouvez corrompre facilement votre programme si vous ne prenez pas garde à cela.
Il existe plusieurs variantes des commandes Peek/Poke qui sont adaptées à la gestion des différents types de données. Pour les chaînes c'est PeekS() / PokeS().
a.s = "test" ; une chaîne de 4 caractères (en fait 4+1 avec le zéro terminal)
b = @a ; b récupère l'adresse mémooire où se trouve la variable a
Debug b ; affiche l'adresse du début de la chaîne en mémoire
Debug a ; affiche la valeur de la variable a
; Autre façon d'afficher cette valeur par PeekS()
; On utilise l'adresse de la variable que l'on transmet à la fonction PeekS() qui
; permet d'afficher le contenu de l'adresse
Debug PeekS(@a)
; Affichage des 2 premiers caractères de la chaîne
Debug PeekS(@a, 2)
PokeS(b, "1234") ; modification d'une zone mémoire en écrivant directement à l'adresse pointée par b
Debug a ; affichage de la valeur de la variable a qui a été modifiée
z.s = "1234567890" ; chaîne de 10 caractères de longueur (10+1 en fait)
PokeS(@z, "abcd") ; écriture de 5 caractères en mémoire (oui pas 5 octets !)
Debug z
PokeS(@z, "ABCD", 2) ; écriture de 2 caractères (+1 pour le zéro final)
Debug z
Pour chaque type de nombre, PureBasic propose des variantes de Peek / Poke. L'utilisation du caractère '@' permet de récupérer l'adresse de la variable en mémoire. ATTENTION : il faut vraiment faire la différence entre les versions 32 et 64 bits des nombres et des environnements lors de la programmation. Par rapport à ce que l'on trouve dans d'autres langages de programmation, la notion d'entier long en PureBasic repose sur des valeurs 32 bits et non 64 bits. Pour gérer l'environnement 64 bits, il faut traiter les nombres au format entier (.i).
a.l = 10
addr.l = @a ; adresse de la variable a exprimée sur 32 bits
PokeB(addr, 2) ; modifie l'adresse mémoire pointée par addr pour y placer la valeur 2
En 64 bits, le code précédent va retourner une erreur car l'adresse obtenue en utilisant '@a' renvoit
le résultat sur 32 bits. Il faut alors utiliser le code de la façon suivante :
a.l = 10
addr.i = @a ; adresse de la variable a exprimée sur 64 bits
PokeB(addr, 2) ; modifie l'adresse mémoire pointée par addr pour y placer la valeur 2
Si vous vous souvenez ce qu'il a été dit sur les systèmes numériques (ou voir Les systèmes numériques) la plus petite unité manipulable par l'ordinateur est le bit (binary digit) qui vaut soit 0 soit 1. En collant 8 bits on obtient un octet (un Byte en bon Anglais) qui peut avoir des valeurs de 0 à 255. En collant 2 octets on obtient un mot de 16 bits pour des valeurs de 0 à 65535. En collant 2 mots on obtient un entier long de 32 bits pour des valeurs entre 0 et 2^32-1. En collant 2 entiers longs on obtient un quad sur 64 bits pour des valeurs entre 0 et 2^64-1.
De base tous les nombres, quels qu'ils soient, sont représentés par une suite de bits (0 ou 1). C'est la façon de gérer ces suites d'unités de valeur qui représente les nombres.
En PureBasic, un nombre binaire est déclaré par b.u = 192
. Il pourrait également se déclarer
sous la forme b.u = %11000000
ou bien, sous une forme hexadécimale b.u = $C0
.
C'est à l'être humain de choisir la représentation qu'il préfère, sachant que cela n'a aucun impact sur
le programme lui-même. Nous avons choisit de considérer les valeurs en tant que nombre non signé. Si nous
voulons utiliser des nombres signés, la déclaration se fera par b.b = 192
mais si on affiche
la valeur 192 affectée à un nombre signé cela nous donnera -64...
L'intérêt des représentations binaires vient de l'usage généralement fait de gestion des flags ou des commutateurs de sélection. Par exemple, si on suppose que la variable 'b' est utilisée pour conserver les différents états pris par le programme (8 états dans l'exemple), nous pourrions préciser que chaque bit composant l'octet va permettre de conserver un état indépendant.
b.a = %10000000 ; valeur de départ avec l'état 7 activé (le dernier en partant de la droite)
b = b | %00001001 ; activation des états 0 et 3 (en comptant à partir de la droite)
Debug b ; va afficher 137 (soit %10001001)
Nous venons de voir que nous pouvions modifier la valeur d'un bit d'un nombre. Mais qu'en est-il pour
mettre 0 à la place de 1 dans ce même nombre ? Dans notre exemple nous souhaitons annuler l'état 3 (bit 4)
b.a = %10001001 ; valeur décimale 137
b = b & %11110111
Debug b ; va afficher 129 (soit %10000001 en binaire)
PureBasic dispose de quelques fonctions pratiques pour convertir les nombres d'une base à une autre, ou bien pour afficher un nombre sous forme de texte ou pour transformer une chaîne de caractères en nombre. Ces fontions sont :
a.s = "1234"
b.l = Val(a)
c.s = Str(b)
; Toutes les lignes suivantes affichent 1234 au format numérique ou textuel
Debug a
Debug b
Debug c
Historiquement il y a toujours eu une différence de stockage des nombres entre les divers processeurs équipant les ordinateurs. Les processeurs Intel et compatibles utilisent le stockage Little Endian. Cela signifie que les octets de poids faible sont stockés en premier, suivis des octets de poids fort. L'autre famille de processeurs (notamment les Motorola) suivent les règles du Big Endian et signifie donc que les octets de poids fort sont stockés en premier, suivis des octets de poids faible. Puisque nous sommes sur une architecture Intel, nous allons nous occuper des nombres en Little Endian.
a.l = 837645420 ; nombre entier long sur 32 bits
b0.i = @a ; emplacement du premier octet en mémoire, bits 0 à 7
b1.i = @a+1 ; emplacement du deuxième octet en mémoire, bits 8 à 15
b2.i = @a+2 ; emplacement du troisième octet en mémoire, bits 16 à 23
b3.i = @a+3 ; emplacement du quatrième octet en mémoire, bits 24 à 31
Debug a
Debug PeekA(b0) ; affichage d'un octet non signé
Debug PeekA(b1)
Debug PeekA(b2)
Debug PeekA(b3)
Debug PeekL(@a) ; affichage du nombre entier long depuis son adresse
; Recomposition du nombre d'origine avec les multiplications des puissances de 2
Debug PeekA(b0) + PeekA(b1)*256 + PeekA(b2)*65536 + PeekA(b3)*16777216
La logique booléenne est très importante en informatique car elle entre dans beaucoup de traitements de conditions. Mr Boole, le mathématicien à l'origine de cette algèbre, a défini un certain nombre d'opérations possibles. Pour les détails, rapportez-vous à la section sur les systèmes numériques du site. Nous allons simplement nous attacher à rappeler les principes de base de la manipulation binaire.
Pour déterminer si une combinaison d'éléments est vraie ou fausse, nous utilisons implicitement la logique booléenne. Celle-ci nous donne plusieurs moyens d'effectuer ces combinaisons :
c1 = #True
c2 = #False
If c1 And c2
Debug "Impossible car c1 est toujours différent de c2 (Vrai est toujours différent de Faux)"
EndIf
If c1 Or c2
Debug "Vrai car au moins une condition est vraie"
EndIf
If c1 XOr c2
Debug "Vrai car les deux conditions sont différentes"
EndIf
If c1 And Not c2
Debug "Vrai car la condition c2 est devenue vraie par l'usage de Not. Et donc Vrai est toujours égal à Vrai"
Debug "Par contre Faux = Faux est toujours une condition Fausse qui ne se réalise pas.
EndIf
On pourrait simplifier les tests en signalant que tout ce qui est 0 est Faux et que tout ce qui est
différent de 0 est Vrai.
c1 = 0
If c1 = 0
Debug "La condition est Vraie"
EndIf
If c1
Debug "La condition est fausse car c1 est égal à 0 et que le test demande à ce que c1 soit Vrai donc différent de 0..."
EndIf
Les opérateurs binaires s'appliquent sur les bits composant les nombres, de la même manière que pour les opérations booléennes. Il n'y a que la nomenclature qui change. Ainsi,
a = %11110000
b = %00111100
Debug Bin(~w) ; Affiche 1111111111111111111111111111111111111111111111111111111100001111
Debug Bin(a & b) ; Affiche 110000
Debug Bin(a | b) ; Affiche 11111100
Debug Bin(a ! b) ; Affiche 11001100
PureBasic affiche les valeurs sans les zéros qui précèdent la valeur mais avec tous les 1. C'est pourquoi
la première ligne qui était %11110000 avant la négation devient %1111111111111111111111111111111111111111111111111111111100001111
après la négation. Pour rappel un nombre affiché %1111 est en fait %0000000000000000000000000000000000000000000000000000000000001111
car les zéros ne sont pas exprimés lorsqu'ils sont devant le nombre...
Certaines opérations de multiplications ou de divisions par 2 peuvent être réalisées plus rapidement en utilisant les décalages. Un décalage à gauche se note par "<<" pour la multiplication et ">>" pour la division.
a = %1011 << 1 ; La valeur de 'a' sera %10110. (en decimal: %1011=11 et %10110=22)
b = %111 << 4 ; La valeur de 'b' sera %1110000. (en decimal: %111=7 et %1110000=112)
c.l = $8000000 << 1 ; La valeur de 'c' sera 0. Les bits supérieurs sont perdus car ils dépassent la capacité du type.
d = 16 >> 1 ; La valeur de 'd' sera 8. (en binaire: 16=%10000 et 8=%1000)
e.w = %10101010 >> 4 ; La valeur de 'e' sera %1010. (en décimal: %10101010=170 et %1010=10).
f.b = -128 >> 1 ; La valeur de 'f' sera -64. -128=%10000000, -64=%11000000. Lors du décalage, le bit le plus fort reste (conservation du signe).
Il faut penser la structure d'un tableau comme une suite de cases dans lesquelles on va placer une valeur.
Il peut y avoir des tableaux à une dimension, à deux dimensions... Avant de pouvoir être utilisé,
un tableau doit être initialisé par la commande Dim en précisant le nom du tableau, son type et une
ou plusieurs valeurs en fonction du nombre de dimensions souhaitées (Dim
; Tableau à une seule dimension
Dim MonTableau(41)
MonTableau(0) = 1
MonTableau(1) = 2
; Tableau à dimensions multiples
Dim TableauMultiple.b(NbColonnes, NbLignes)
TableauMultiple(10, 20) = 10
TableauMultiple(20, 30) = 20
; Redimensionnement du tableau
Dim MonTableau.l(1) ; nous avons 2 éléments
MonTableau(0) = 1
MonTableau(1) = 2
ReDim MonTableau(4) ; Maintenant nous avons 5 éléments
MonTableau(2) = 3
For k = 0 To 2
Debug MonTableau(k)
Next
PureBasic est un Basic évolué également dans le sens où il peut interagir avec l'environnement fenêtré sur lequel il fonctionne. Précédemment nous nous sommes contentés d'utiliser les affichages produits et gérés par la console. Nous allons maintenant nous préoccuper de la manipulation des fenêtres.
OpenConsole()
PrintN("Hello World")
Repeat
Until Inkey() <> ""
CloseConsole()
If OpenWindow(0,100,100,320,240,"Hello World",#PB_Window_SystemMenu|#PB_Window_SizeGadget)
Repeat
Until WaitWindowEvent()=#PB_Event_CloseWindow
EndIf
Chaque objet de PureBasic est référencé par une valeur. Celle-ci peut appartenir uniquement à PureBasic et utilisé pour sa gestion propre, ou bien provenir du système d'exploitation sous-jacent afin de pouvoir interagir avec lui. Pour chaque objet d'un même type, les numéros doivent être différents. Mais vous pouvez avoir un numéro identique entre un bouton et une liste car ce ne sont pas des objets identiques.
numberWindow = OpenWindow(#PB_Any, 100, 100, 100, 100, "Fenêtre")
handleWindow = WindowID(numberWindow)
Comme pour tous les systèmes multitâches (Windows, Linux, MacOS, Amiga, ...), le fonctionnement d'un programme repose sur une boucle principale qui se répète tant que le programme est en fonction : la boucle événementielle. Cette gestion par événements permet de mettre en place une gestion cohérente entre les divers programmes en fonctionnement dans l'ordinateur et en respectant le postulat que chaque programme se verra donné un temps minimum d'exécution.
Avec les anciens systèmes comme MS-DOS, il n'y avait qu'un seul programme possible en mémoire au même moment. Aujourd'hui, des centaines de programmes peuvent fonctionner en simultané, tout cela grâce aux événements.
Tous les objets de PureBasic respectent ce paradigme de programmation, qu'ils soient graphiques (comme des boutons, labels, listes...) ou non (comme les timers) et permettent de piloter le programme en envoyant des signaux au programme. Ces événements vont être traités par le programme dans la boucle d'événements. Ce principe permet d'être "system-friendly" en laissant du temps aux autres programmes lorsqu'aucun événement n'est disponible pour votre programme.
Donc globalement votre programme va passer la majorité de son temps à attendre des événements, même si vous avez l'impression que vous êtes seul au monde avec votre ordinateur et que le système d'exploitation ne s'occupe que de vous... PureBasic dispose typiquement de 2 fonctions pour cette gestion :
win = OpenWindow(1, 100, 100, 200, 200, "Evenements")
fini = 0
Repeat
event = WindowEvent()
If event
Select event
Case #PB_Event_CloseWindow
fini = 1
Debug "Fermeture de la fenêtre."
EndSelect
Else
Delay(2)
EndIf
Until fini
win = OpenWindow(1, 100, 100, 200, 200, "Evenements")
fini = 0
Repeat
event = WaitWindowEvent()
Select event
Case #PB_Event_CloseWindow
fini = 1
Debug "Fermeture de la fenêtre."
EndSelect
Until fini
Il y a de très nombreuses sources d'émission de messages comme les fenêtres, les boutons, les menus, les timers, la souris, le clavier et d'autres encore. Pour quasiment chaque source PureBasic dispose d'une procédure permettant de déterminer quel type d'objet a émis le(s) message(s) afin d'apporter les traitements adéquats : EventMenu(), EventGadget(), EventWindow(), EventTimer()... Chaque procédure retourne l'identificateur de l'objet source du message.
If OpenWindow(0, 0, 0, 230, 90, "Exemple de gestion des évènements...", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
; Création de deux gadgets : 1 bouton et un checkbox
ButtonGadget (1, 10, 10, 200, 20, "Cliquez-moi")
CheckBoxGadget(2, 10, 40, 200, 20, "Cochez-moi")
Repeat
Event = WaitWindowEvent() ; attente passive
Select Event
Case #PB_Event_Gadget ; on a reçu un message en provenance d'un gadget PureBasic
Select EventGadget() ; récupère le numéro du gadget dans la liste interne de PureBasic
Case 1 : Debug "Bouton cliqué !"
Case 2 : Debug "Case à cocher cliquée !"
EndSelect
EndSelect
Until Event = #PB_Event_CloseWindow
EndIf
Il est évident que nous ne traiterons pas de tous les objets graphiques que PureBasic met nativement à notre disposition, mais voici les principaux éléments à prendre en compte pour gérer correctement ses gadgets :
Nous ne dresserons pas la liste complète des gadgets disponibles mais vous pourrez utiliser ceux que l'on rencontre généralement : boutons, listes, checkbox, combobox, menus, zone de dessin, et bien d'autres encore. Je vous renvoie à la documentation toujours bien faite pour obtenir tous les renseignements sur ces objets.
La seule chose que nous allons détailler est l'usage de layouts pour réaliser des compositions de fenêtres qui puissent être élégantes et permettre aux gadgets de s'adapter automatiquement à la dimension de la fenêtre.
Généralement lorsque l'on produit une interface graphique, les objets ne sont pas redimensionnables aisément, sauf à passer du temps à générer des gestions sans fin d'événements de redimensionnement de fenêtre. Ceux qui ont utilisé Gtk ou Tcl savent de quoi je parle. C'était également le cas avec PureBasic avant la version 5 qui a introduit un gestionnaire de layout basé sur le langage de description XML.
Tout se passe avec seulement une fonction nommée OpenXMLDialog() qui va traiter une chaîne XML contenant une description d'interface et rendre le tout redimensionnable ou non en fonction du contenu de ce fichier. Vous pourrez même y inclure les diverses actions à effectuer en fonction des événements survenant sur les différents objets de la fenêtre.
Cet article est inspiré du site de Bluez dont les coordonnées sont définies dans la colonne d'accès rapide du site. J'ai essayé de simplifier et d'apporter des modifications intéressantes sans vous noyer sous les définitions. Je ne suis pas sûr d'y être parvenu...