Énumération, de simples constantes ?

Introduction

Ce tutoriel propose de comprendre les énumérations en programmation et leur périmètre d'intervention.

Il a été pensé en Visual Basic Application (VBA) et plus précisément sur Microsoft Excel.

Il est néanmoins adaptable en tous langages, ce ne sont que des principes algorithmiques.

Retour sur les constantes

Une des bonnes pratiques en matière de programmation est d'utiliser des constantes partout où l'on écrit une valeur dans le code.

L'instruction MsgBox "Fichier sauvegardé avec succès." sera avantageusement remplacée par :

Const cs_Message As String = "Fichier sauvegardé avec succès."

MsgBox cs_Message

Ainsi si le message change, le code fonctionne toujours principalement si cette valeur est utilisée à plusieurs endroits du programme et surtout on s'offre l'opportunité de stocker un jour ces valeurs dans un fichier de configuration.

Constantes multiples

Le besoin est fréquent d'avoir plusieurs constantes qui représentent le même élément :

Pour les jours de la semaine, j'ai besoin de 7 constantes.

Const cs_Lundi = 1

Const cs_Mardi = 2

Etc.

Je peux faire de même pour des mois, des directions (Nord - Sud - Est - Ouest), du matériel (Ordinateur - Imprimante - Clavier - etc.).

La gestion d'un tel système n'est pas toujours aisée à maintenir et faire fonctionner.

Les énumérations

Une énumération permet de regrouper des constantes de type entier et de même nature au même endroit.

Les déclarations de constantes précédentes peuvent s'écrire dans une énumération à l'aide d’une instruction « Enum ».

Enum ESemaine

  eLundi

  eMardi

  eMercredi

  eJeudi

  ...

End Enum

Portée

Une énumération peut être déclarée publique :

Public Enum ESemaine

Elle peut avoir une portée privée :
Private Enum ESemaine

Ce n'est pas un type de données, mais une déclaration, elle ne peut être déclarée dans une procédure - fonction - propriété.

Usage

Déclaration

Comme les autres types de données, avant usage, il faut déclarer une variable de ce type :
Dim MonJour As ESemaine

Affectation

Les énumérations s’utilisent comme n'importe quelle variable, à ceci près que l'on commence par donner le nom de l'énumération suivi d’un point.
MonJour = ESemaine.

Intellisence

Quand on saisit le point, l'éditeur Visual Basic, nous propose les valeurs, que nous appellerons « données membres ».

Intellisence énumération
Figure 1 : Intellisence VBA.

Il ne reste qu’à choisir sa donnée.
MonJour = ESemaine.eJeudi.

Il n'est pas obligatoire de préciser le nom de l'énumération sauf s’il existe une constante ou une donnée membre d'une autre énumération qui porte le même nom.

MonJour = eJeudi    ' Possible s'il n'y a pas d'ambiguïtés.

MsgBox MonJour    ' Affiche 3.

S'il est effectivement possible de passer par une variable, ce n'est pas obligatoire et on peut utiliser l'énumération tel-quel :
MsgBox ESemaine.eLundi.

Définir les valeurs

Mais quelles valeurs peuvent bien afficher les données membres de l'énumération ?

Effectivement, à aucun moment nous n'avons défini ces valeurs.

Tout d'abord, c'est forcément un nombre de type entier (Byte - Integer - Long).

Visual Basic attribue des valeurs par défaut.

Par défaut, le premier membre de l'énumération prend la valeur zéro (0) :
ESemaine.eLundi = 0.

Les autres membres sont incrémentés de 1 par rapport à la valeur précédente.

ESemaine.eMardi = 1

ESemaine.eMercredi = 2

ESemaine.eJeudi = 3

L'utilisateur peut fixer ses valeurs personnelles

Enum MyEnum

  eConstA = 7 ' Prend la valeur => 7

  eConstB ' Prend la valeur précédente + 1 => 8

  eConstC ' Prend la valeur précédente + 1 => 9

  eConstD = 12 ' Prend la valeur => 12

  eConstE ' Prend la valeur précédente + 1 => 13

End Enum

Le second membre n'a pas de valeur fixée, il prend donc la valeur précédente incrémentée de 1 soit 8.

Pour le 3° membre c'est la même chose, et il vaut 9.

La 4° constante voit sa valeur attribuée par l'utilisateur et la 5° valeur est incrémentée de 1 (13).

Ce qui précède revient donc au même que d'écrire :

Enum MyEnum

  eConstA = 7

  eConstB = 8

  eConstC = 9

  eConstD = 12

  eConstE = 13

End Enum

Valeurs identiques

Plusieurs constantes peuvent porter la même valeur.

Enum MyEnum

  eConstA = 1

  eConstB = 1

  eConstC = 0

  eConstD = 1

  eConstE = 0

  eConstF = 0

  eConstG = 1

  eConstH = 0

End Enum

Comme ici, nous pouvons créer des champs de bits, ou systèmes à deux états (ouvert/fermé).

Une constante peut en utiliser une autre

Une donnée de l'énumération peut fixer sa valeur à partir d'une autre tant que l'expression reste constante.

Enum MyEnum

  eConstA = 7 ' Prend la valeur => 7

  eConstB ' Prend la valeur précédente + 1 => 8

  eConstC = eConstA + 2 ' Prend la valeur => 9

  eConstD = 12 ' Prend la valeur => 12

  eConstE = eConstD + 3 ' Prend la valeur => 15

End Enum

Ordre des données membres

Il n'y a aucune obligation à définir des valeurs en ordre croissant et l'énumération suivante est valide :

Enum MyEnum

  eConstA = 12 ' Prend la valeur => 12

  eConstB ' Prend la valeur précédente + 1 => 13

  eConstC = 7 ' Prend la valeur => 7

End Enum

La valeur peut-être négative

Enum MyEnum

  eConstA = -7 ' Prend la valeur => -7

  eConstB ' Prend la valeur précédente + 1 => -6

End Enum

Constante Double

Il est possible de renseigner la constante avec un type double, mais à l'usage, elle est convertie en entier.

Enum MyEnum

  eConstA = 12.4 ' Prend la valeur => 12

  eConstB ' Prend la valeur précédente + 1 => 13

End Enum

Type de données d'une énumération

Les valeurs de l'énumération sont de type long.

Dim MonJour As ESemaine

MonJour = ESemaine.eLundi

Vérification du type de données

TypeName(MonJour) ' Retourne "Long".

VarType(MonJour) ' Retourne vbLong 3 Entier long

Visual Basic ne tient pas compte du type

Dim iJour As Integer, MonJour As ESemaine

MonJour = ESemaine.eLundi

La conversion d'un type énumération en un type entier est transparente.

iJour = MonJour ' Pas besoin de conversion de type.

Il est bien sûr possible de convertir explicitement.

iJour = CInt(MonJour)

Visual Basic ne respecte pas la casse

Comme pour les étiquettes Goto (contrairement à la plupart des instructions du langage), quand on appelle dans le code une constante de notre énumération, l'écrire en minuscule, modifiera la casse dans la déclaration.

Enum ESemaine ' Casse Majuscule

  eLundi = 1 ' Casse Majuscule

End Enum

Dim MonJour As esemaine

MonJour = esemaine.elundi ' Casse en minuscule, modifie la casse de la donnée membre dans la déclaration.

Enum ESemaine ' La casse reste en majuscule sur le nom de l’énumération.

elundi = 1 ' La casse des données membres est modifiée.

End Enum

Associer une chaine

Lorsque l'on a des constantes numériques comme les jours de la semaine, immanquablement, nous souhaitons y associer un texte et dans les forums les questions à ce sujet sont nombreuses.

Enum ESemaine

  eLundi = 1

  eMardi

  eMercredi

  eJeudi

  eVendredi

  eSamedi

  eDimanche

End Enum

Synchroniser le nom du jour

ESemaine.eLundi = "Lundi", ESemaine.eMardi = "Mardi", etc.

Structures conditionnelles (If/Case)

C'est sûrement la méthode la moins productive, mais selon les valeurs dans l'énumération, il n'y a guère de choix.

Dim monJour As ESemaine

MonJour = ESemaine.eMardi

Select Case MonJour

  Case ESemaine.eLundi ' Ou Case 1

  MsgBox "Lundi"

  Case ESemaine.eMardi

  MsgBox "Mardi"

End Select

Fonction Choose

Si les valeurs des constantes sont incrémentées de 1, alors c'est un jeu d'enfant d'associer un contenu.

MonJour = ESemaine.eJeudi

MsgBox Choose(MonJour, "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")

Avec la variable "MonJour" qui prend la valeur "ESemaine.eJeudi" (soit la valeur 4), la fonction Choose() retournera "Jeudi".
L'indice de départ de la fonction « Choose » est 1.

Utiliser un Array

C'est le même principe que la fonction Choose() à la différence que dans un Array, l'indice commence à 0.

Dim LibJours As Variant

LibJours = Array("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")

Dim monJour As ESemaine

MonJour = ESemaine.eJeudi

MsgBox LibJours(MonJour) ' Affiche "Jeudi" en considérant que l'énumération commence à 0.

Parcourir une énumération

Quand les valeurs des constantes s'incrémentent, une boucle permet de parcourir toutes les valeurs de l'énumération.

Dim MonJour As ESemaine, i As ESemaine

MonJour = ESemaine.eJeudi

For i = ESemaine.eLundi to ESemaine.eDimanche

  If (i = MonJour) Then

    If MonJour < ESemaine.eSamedi) Then

      MsgBox "Pas de chance, aujourd'hui tu bosses !"

      Exit For

    End If ' MonJour < ESemaine.eSamedi

  End If ' i = MonJour

Next i

Prendre une partie de l'énumération

Dim MonJour As ESemaine, i As ESemaine

MonJour = ESemaine.eJeudi

For i = ESemaine.eLundi to ESemaine.eVendredi

  If (MonJour = i) Then

    MsgBox "Pas de chance, aujourd'hui tu bosses !"

  End If ' MonJour = i

Next i

Membres masqués

Comme vu plus haut, les membres de l'énumération sont visibles dans l'éditeur Visual Basic par l’intermédiaire d’Intellisense.

Il est possible de masquer certains membres en commençant leur nom par un underscore (_) et en l’entourant de crochets.

Enum ESemaine

  [_First] ' Prend la valeur par défaut 0.

  eLundi = 1

  eMardi

  eMercredi

  eJeudi

  ...

  [_Last] ' Dernier membre de l'énumération + 1 soit eDimanche + 1 = 8.

End Enum

Membre d'enumeration masque en VBA
Figure 2 : Membre d'énumération masquées en VBA.

Ce n'est pas parce qu’ils ne sont pas visibles dans l'éditeur, qu'ils ne sont pas utilisables sans quoi cela n'aurait aucun intérêt.

Si on connait leur existence, on peut les insérer dans son code.

C'est une technique éprouvée pour initier des bornes. C'est idéal pour parcourir l'énumération complète.

Dim i As ESemaine

For i = ESemaine.[_First] To ESemaine.[_Last]

  ...

Rôle d'une énumération

Considérons l'énumération suivante :

Enum UneEnum

  eConstA = 12

  eConstB = 3

  eConstC = 577

  eConstD = 602

  eConstE = 17259

End Enum

Et la boucle suivante :

Dim myEnum As UneEnum, i As UneEnum

myEnum = UneEnum.eConstE

For i = UneEnum.eConstA To UneEnum.eConstE

  If (i = UneEnum.eConstE) Then

    ' Faire quelque chose

  End If ' i = UneEnum.eConstE

next i

Pour 5 constantes, soit 5 tours de boucle avec une énumération incrémentale, ici, il faut 17259 tours de boucles. C'est tout simplement inacceptable !

À remarquer en outre, que si on cherche UneEnum.eConstB, on ne le trouvera jamais en commençant une boucle à 12 !

Est-ce à dire qu'une énumération est une façon élégante d'organiser et documenter son programme et que l'on ne peut rien en faire d'autre ?

Dans ce sens oui, et si les valeurs ne sont pas issues d'une séquence identique d'une constante à l'autre, il existe d'autres façons d'arriver au même résultat.

Utiliser des Array

Je préfère dans ce cas utiliser un Array de constantes. C'est bien plus élégant, simple et efficace.

MyArray = (12, 3, 577, 602, 17259)

Les collections

Un peu lourd et surtout n'importe qui peut enlever, rajouter, modifier les données.

Piloter les données

Je viens de construire un Array, mais rien ne me dit à quoi correspond la valeur 577 de mon tableau.

Et si je faisais une énumération qui m'indique le rôle de chaque valeur ?

Enum EPopulation

  [_First] ' -1

  MaFamille = 0

  MaMaison = 1

  NosDeputes = 2

  LeBourg = 3

  LeVillage = 4

  [_Last]

End Enum

Il devient facile d'aller chercher une valeur dans notre tableau en fonction de la valeur de la constante.

Dim myPop As EPopulation

myPop = EPopulation.LeBourg

MsgBox MyArray(myPop)

C'est ici qu'intervient le vrai rôle des énumérations, ce pour quoi elles ont été créées.

Il ne faut pas envisager une énumération comme un simple regroupement de constantes.

Dans le cas présent, elles jouent le rôle de « pointeur » vers des données. Elles se comportent comme un centre d'aiguillage.

D'autres usages

Les énumérations sont depuis le début de la programmation un élément important trop souvent ignoré des néophytes qui n'en saisissent pas toute la portée.

Les propriétés visibles dans l'inspecteur de l'éditeur Visual Basic sont issues d'énumérations.

Les combinaisons de boutons et options de la fonction MsgBox sont des énumérations.

Elles sont particulièrement indiquées pour conserver des bits.

Enum EChar

  [_First]

  Bit1 = 1 ' Bit 1 = 1

  Bit2 = 0 ' Bit 2 = 2

  Bit3 = 0 ' Bit 3 = 4

  Bit4 = 0 ' Bit 4 = 8

  Bit5 = 0 ' Bit 5 = 16

  Bit6 = 0 ' Bit 6 = 32

  Bit7 = 1 ' Bit 7 = 64

  Bit8 = 0 ' Bit 8 = 128

  [_Last]

End Enum

Ici, nous avons la valeur 65 qui est le code ASCII du caractère "A" majuscule.

Opérations et énumération

Il est possible d’additionner des constantes d'énumération, ce sont des valeurs comme les autres.

Dim WE As ESemaine

WE = ESemaine.eSamedi + ESemaine.eDimanche

Si Samedi vaut 6 et dimanche 7 alors la variable WE prendra la valeur 13.

Calculs ambigus

Soit une variable MesJours égale à ESemaine.eLundi + ESemaine.eMardi, ce qui donne le résultat 3 (1 + 2), or c'est aussi la valeur de ESemaine.eMercredi.

En pareil cas, calculer avec ces constantes devient hasardeux.

Utiliser une autre séquence

Est-ce à dire qu'il faut abandonner l'idée de calculer avec des énumérations ?

Avoir des constantes incrémentées de 1 c'est bien, mais peut-être qu'une autre suite est possible.

Il faudrait que quel que soit les combinaisons des membres de l'énumération, la somme de leurs valeurs ne donne jamais le même résultat.

Cette suite existe, il suffit que la valeur de la constante soit le double de la constante précédente et que la première commence à 1 comme dans l'énumération suivante :

Enum ESemaine

  [_First]

  eLundi = 1

  eMardi = 2

  eMercredi = 4

  eJeudi = 8

  eVendredi = 16

  eSamedi = 32

  eDimanche = 64

[_Last]

End Enum

Bien sûr cette suite n'est pas choisie au hasard. Avec elle, nous pouvons additionner n'importe quelle valeur, nous ne trouverons jamais deux fois le même résultat.

En reprenant l'exemple précédent ESemaine.eLundi + ESemaine.eMardi nous avons toujours 3 pour résultat, mais maintenant, ESemaine.eMercredi prend la valeur 4.

Écritures

Comme dans l'exemple précédent, on peut écrire directement les valeurs en "dur" eLundi = 1 - eMardi = 2 - etc, c'est le plus simple.

Mais on peut aussi s'appuyer sur la constante précédente :

Enum ESemaine

  [_First]

  eLundi = 1 ' 1

  eMardi = eLundi * 2 ' 2

  eMercredi = eMardi * 2 ' 4

  eJeudi = eMercredi * 2 ' 8

  eVendredi = eJeudi * 2 ' 16

  eSamedi = eVendredi * 2 ' 32

  eDimanche = eSamedi * 2 ' 64

  [_Last]

End Enum

La valeur est un multiple de 2, donc l'écriture suivante est valide bien que peu ergonomique :

Enum ESemaine

  [_First]

  eLundi = 1 ' 1

  eMardi = 2 * 1 ' 2

  eMercredi = 2 * 2 ' 4

  eJeudi = 2 * 2 * 2 ' 8

  eVendredi = 2 * 2 * 2 * 2 ' 16

  eSamedi = 2 * 2 * 2 * 2 * 2 ' 32

  eDimanche = 2 * 2 * 2 * 2 * 2 * 2 ' 64

  [_Last]

End Enum

En d'autre terme, les valeurs sont des nombres en puissance de 2 et l'exposant du nombre est l'incrément de l'exposant précédent.

Enum ESemaine

  [_First]

  eLundi = 2 ^ 0 ' 1 c'est inscrit dans le marbre comme pi = 3.14...

  eMardi = 2 ^ 1 ' 2 * 1

  eMercredi = 2 ^ 2 ' 2 * 2

  eJeudi = 2 ^ 3 ' 2 * 2 * 2

  eVendredi = 2 ^ 4 ' 2 * 2 * 2 * 2

  eSamedi = 2 ^ 5 ' 2 * 2 * 2 * 2 * 2

  eDimanche = 2 ^ 6 ' 2 * 2 * 2 * 2 * 2 * 2

  [_Last]

End Enum

Nombre de constantes dans l'énumération

Au-delàs de 2 ^ 30 nous dépassons les capacités de stockage d'un long.

Cela revient à dire qu'une énumération construite sur ce modèle est limitée à 31 constantes.

Ce nombre peut être outrepassé, mais au prix de pirouettes périlleuses.

Nous tirerons avantage à créer une constante publique qui contient cette limite.

Nous allons créer plusieurs fonctions pour manipuler les énumérations et cette constante sera utile.

Const cs_EnumItemMax = 30

Traitement itératif d'une énumération

Avec une boucle sur notre énumération, nous retombons sur un souci déjà rencontré.

Pour boucler sur 7 constantes, il faut partir de 1 jusqu'à, 64 et avec 2 ^ 30 nous bouclons jusqu'à 1073741824 pour uniquement 30 constantes, c'est tout simplement inepte !

For i = ESemaine.eLundi To ESemaine.eDimanche

  ' Faire quelque chose...

Itération sur une énumération

Encore une fois, faut-il abandonner l'idée de parcourir des énumérations ?

C'est sans compter que les puissances de 2 ont plus d'un tour dans leur sac et des propriétés vraiment intéressantes.

Connaitre le nombre de constantes qui composent l'énumération permet d'avoir des boucles qui ne font que le nombre de tours nécessaires.

Nous avons besoin de deux éléments :

  • La valeur de la constante la plus élevée.
    C'est ici que la constante [_Last] va être d'une aide précieuse.
  • Le nombre maximum de données autorisées dans une énumération.
    Nous utiliserons la constante créée plus haut "cs_EnumItemMax".

Public Function CountConstant(ByVal argConstant As Long, Optional ByVal argMax As Byte = cs_EnumItemMax) As Byte

Dim Puissance As Long, PuissanceMax As Long, FlagSortie As Long

CountConstant = 0

If (argConstant = 1) Then

  CountConstant = 1

ElseIf (argConstant > 0) Then

  ' Calcul de la valeur maximale possible avec le nombre de constantes maximal autorisé.

  PuissanceMax = 2 ^ argMax

  ' Dernière valeur de l'énumération.

  FlagSortie = argConstant

  If (argConstant Mod 2 <> 0) Then

    FlagSortie = FlagSortie - 1

    End If ' argConstant Mod 2 <> 0

  For Puissance = 0 To PuissanceMax

    If (2 ^ Puissance > FlagSortie) Then

      Exit For

    End If ' 2 ^ Puissance > FlagSortie

  Next Puissance

  CountConstant = Puissance

End If ' argConstant = 1

End Function

L'appel à la fonction est des plus classiques :
MsgBox CountConstant(ESemaine.[_Last]) ' Affiche 7.

Il est possible de passer n'importe quelle valeur de l'énumération :
MsgBox CountConstant(ESemaine.eJeudi) ' Affiche : 4. Jusqu'à ESemaine.eJeudi, il y a 4 jours.

Ne pas oublier que VB ne fait pas de contrôle de type avec les énumérations.

L'argument peut être de type "Long" et non pas spécialisé vers une énumération. Ainsi la même fonction marche quel que soit l'énumération.

Function CountConstant(ByVal argLast As Long) As Byte

  MsgBox CountConstant(65) ' Ou MsgBox CountConstant(ESemaine.[_Last])

Maintenant que nous connaissons le nombre de constantes, il devient facile de réaliser une boucle :

For i = ESemaine.[_First] To CountConstant(ESemaine.[_Last]) -1

  MsgBox i

Exposant d'une constante

Connaissant le nombre de données membres, il est simple de retrouver l'exposant d'une valeur puisque c'est le nombre de constantes trouvées moins 1.

Function Exposant(byVal argEnum As long) As Byte

  Exposant = CountConstant(argEnum) - 1

End Function

MsgBox Exposant(ESemaine.eJeudi) ' Affiche 3 => ESemaine.eJeudi = 2 * 2 * 2 = 2 ^ 3.

Autre façon de le calculer

Function ExposantBis(ByVal argEnum As Long) As Byte

  Dim Nombre As Long

  Nombre = argEnum

  ExposantBis = 0

  Do While Nombre >= 2

    Nombre = Nombre / 2

    ExposantBis = ExposantBis + 1

  Loop

End Function

MsgBox Exposant(ESemaine.eJeudi) ' Retourne 3 => 2 * 2 * 2 = 2 ^ 3 = 8.

Valeur de l'exposant de la constante

En connaissant l'exposant, nous pouvons retrouver la valeur associée.

Calculer la valeur correspondante à un exposant n'est pas sorcier, c’est 2 puissance exposant :

Function ValueByExposant(ByVal argExposant As Byte) As Long

  ValueByExposant = 2 ^ argExposant

End Function

MsgBox ValueByExposant(3) ' Retourne 8 soit eSemaine.eJeudi.

Associer des données

Trouver le texte associé à une constante

Avec un tableau de chaine, le texte à retrouver est le texte dont l'indice est l'exposant.

Dim MesJours As Variant

MesJours = Array("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")

MsgBox MesJours(Exposant(ESemaine.eJeudi))

Trouver la constante associé à un texte

Function FindEnumFromText(ByRef argArray As Variant, ByVal argString As String) As Long

  Dim i As Long, Exposant As Byte

  FindEnumFromText = 0

  For i = LBound(argArray) To UBound(argArray) ' Parcourir les chaines

    If (argArray(i) = argString) Then ' On a trouvé.

      Exposant = i ' L'exposant de notre constante est l'indice du tableau.

      Exit For

    End If ' argArray(i) = argString

  Next i

  FindEnumFromText = ValueByExposant(Exposant) ' Avec l'exposant on retrouve la valeur de la constante.

End Function

MsgBox FindEnumFromText(MesJours, "Jeudi") ' Retourne 8 (ESemaine.eJeudi).

J'utilise ici un simple tableau de chaines, mais rien n'empêche de manipuler un tableau de type (structures).

Notre énumération devient un réel système de pilotage des données.

Naviguer dans l'énumération

Atteindre la première valeur

C'est ESemaine.[_First] + 1 ou plus simplement 1..

Atteindre la dernière constante

De la même manière nous utilisons ESemaine.[_Last] - 1..

Constante précédente

C'est la donnée membre divisée par 2.
Cette technique simpliste s'appelle un décalage de bits à droite..

Elle a déjà été utilisée pour retrouver l'exposant dans la fonction "ExposantBis(...)".

Function PositionPrevious(ByVal argEnum As Long) As Byte

  PositionPrevious = argEnum / 2 ' Décalage droite.

End Function

MsgBox PositionPrevious(ESemaine.eJeudi) ' Retourne 4 (ESemaine.eMercredi).

Constante suivante

À contrario, pour retrouver la valeur suivante, on fait un décalage de bits à gauche en multipliant la valeur par 2.

Function PositionNext(ByVal argEnum As Long) As Long

  PositionNext = argEnum * 2 ' Décalage gauche.

End Function

MsgBox PositionNext(ESemaine.eJeudi) ' Retourne 16 (ESemaine.eVendredi).

Additionner des constantes

Nous avons vu qu'avec ce système, nous pouvions additionner des valeurs sans risque de tomber sur des doublons.

Dim WE As ESemaine

WE = ESemaine.eSamedi + ESemaine.eDimanche ' 96 (32 + 64)

C'est ce que fait MsgBox quand on écrit vbCritical + vbOKOnly + vbMsgBoxHelpButton et l'A.P.I. Windows fait un usage intensif de ce concept.

Opérateur Or

Bien que le signe plus soit admis pour combiner les valeurs, préférez l'opérateur "Or".

Dim WE As ESemaine

WE = Esemaine.eSamedi Or ESemaine.eDimanche ' Retourne 96 (32 + 64).

Il serait d'alleurs plus juste d'utiliser MsgBox de cette manière.

vbCritical Or vbOKOnly Or vbMsgBoxHelpButton ' vbCritical = 16, vbOKOnly = 0, vbMsgBoxHelpButton = 16384 (2 ^ 14).

Toujours utiliser l'opérateur "Or" pour ajouter des données à l'énumération.

L'opérateur plus (+) fonctionne bien lors de l'initialisation, mais lors de l'ajout à une énumération existante le résultat risque de ne pas être celui escompté.

Dim WE As ESemaine

WE = ESemaine.eSamedi + ESemaine.eDimanche ' Ok, initialisation.

WE = WE + ESemaine.eVendredi ' Pas sûr.

Si je veux rajouter un vendredi de RTT, l'usage de l'opérateur Or est requis :

WE = WE Or ESemaine.eVendredi) ' Ok, sûr ! Retourne 112 (16 + 96).

Valeur totale de l'énumération

Le total des valeurs (soit tous les jours de la semaine) c'est l'ensemble des valeurs :

Dim ValueTotal As Long

ValueTotal = ESemaine.eLundi Or ESemaine.eMardi Or ESemaine.eMercredi Or ESemaine.eJeudi Or ESemaine.eVendredi Or ESemaine.eSamedi Or ESemaine.eDimanche.

Ce qui retournera la valeur 127 conforme aux propriétés du binaire comme je l'explique sur mon blog Pompe au Net.

Cette valeur peut être calculée autrement. C'est la dernière valeur ESemaine.eDimanche multipliée par 2 à laquelle nous enlèvons 1 :

ValueTotal = (ESemaine.eDimanche * 2) - 1.

ValueTotal = ((ESemaine.[_Last] - 1) * 2) - 1.

Tester l'énumération

Tester une seule constante

Rappelons-nous qu'une constante est de type long. On peut donc lui passer n'importe quelle valeur même si elle n'existe pas dans l'énumération.

Dim MonJour As ESemaine

MonJour = 23564 ' VB accepte sans broncher !.

Nos deux membres cachés vont limiter l'impact de ce qu'il faut bien appeler un non-sens.

If (MonJour <= ESemaine.[_First]) Then ' Erreur => Trop petit !.

If (MonJour >= ESemaine.[_Last]) Then ' Erreur => Trop grand !.

Tester une combinaison à l'aide de l'opérateur And

Je souhaite savoir si le vendredi est compté comme week-end, l'opérateur "And" permet de savoir si une valeur est dans une énumération

Cet opérateur retourne vrai si la valeur fait partie de l'énumération et faux en cas inverse.

Dim WE As ESemaine

WE = Esemaine.eSamedi Or ESemaine.eDimanche ' Retourne 96 (32 + 64)..

MsgBox WE And ESemaine.eVendredi ' False, eVendredi n'est pas dans le nombre..

MsgBox WE And ESemaine.eSamedi ' True, eSamedi est bien dans la variable WE..

Quand les deux existent, c'est leur somme qui est retournée :

MsgBox WE And (ESemaine.eSamedi Or ESemaine.eDimanche) ' Retourne 96 (32 + 64)..

Quand un seul existe, l'expression renvoie sa valeur :

MsgBox WE And (ESemaine.eVendredi Or ESemaine.eSamedi) ' Retourne ESemaine.eSamedi (32)..

Quand aucun n'existe, la valeur 0 est renvoyée :

MsgBox WE And (ESemaine.eJeudi Or ESemaine.eVendredi) ' Retourne 0..

Supprimer une valeur de l'énumération

Nous avons la constante suivante WE = ESemaine.eVendredi Or ESemaine.eSamedi Or ESemaine.eDimanche à laquelle je souhaite retiré le vendredi.

Il suffit de combiner les opérateurs "And" et "Not" :

WE = WE And Not ESemaine.eVendredi ' We n'est plus composé que de eSamedi et eDimanche.

Switch de valeurs (Xor)

L'opérateur XOr est un peu particulier, en ce sens que son comportement change selon la composition de l'énumération.

XOr ajoute une valeur si elle n'existe pas dans la constante, ou l'enlève si elle existe déjà.

C'est utile pour basculer d'un état à l'autre comme on peut le faire avec l'opérateur Not :

btnA.Enabled = Not btnA.Enabled.

Reprenons notre déclaration sur les weeek-end :

Dim WE As ESemaine

WE = ESemaine.eSamedi Or ESemaine.eDimanche

WE = WE Xor ESemaine.eVendredi

Comme eVendredi n'existe pas dans l'énumération WE, il est rajouté. La constante devient ESemaine.eVendredi Or ESemaine.eSamedi Or ESemaine.eDimanche.

WE = WE Xor ESemaine.eVendredi

Comme ESemaine.eVendredi existe dans l'énumération ESemaine, il est supprimé. La constante devient Esemaine.eSamedi Or ESemaine.eDimanche.

Décomposer un nombre

Avoir la valeur 96 dans la variable WE n'apporte pas grand-chose si l'on ne sait pas retrouver les valeurs qui la composent à savoir ESemaine.eSamedi Or ESemaine.eDimanche.

Une solution est d'utiliser un tableau de booléens qui contient "True" si l'élément est sélectionné, "False" en cas inverse.

Nous créons une fonction qui attend :

  • la valeur maximale de l'énumération (typiquement le membre masqué [_Last])
  • la valeur du nombre composé (par exemple eJeudi Or eSamedi).

La fonction retournera le tableau de Booléens.

Function Decode(ByVal argLast As Long, ByVal argValue As Long) As Boolean()

  Dim m_Count As Byte, Puissance As Long, i As Long

  Dim ExistConst() As Boolean ' Le tableau qui sera retourné.

  Puissance = argValue ' La valeur à décomposer.

  m_Count = CountConstant(argLast) ' Nombre de constantes pour dimensionner le stockage.

  ReDim ExistConst(m_Count) ' On stocke ici.

  For i = 1 To m_Count ' Pour toutes les constantes.

    ExistConst(i - 1) = -1 * (Puissance Mod 2 ^ i) \ 2 ^ (i - 1) ' On calcule.

  Next

  Decode = ExistConst

End Function

L'appel de la fonction

Dim mySelection() As Boolean ' Tableau des sélections.

' On passe la valeur max. de l'énumération et les valeurs sélectionnées.

mySelection = Decode(ESemaine.[_Last], ESemaine.eLundi Or ESemaine.eVendredi)

' Il ne reste qu'à lire notre tableau.

Dim i As Long

For i = LBound(mySelection) To UBound(mySelection) - 1

  MsgBox mySelection(i)

Next i

Le tableau retournera les booléens suivants :

Résultats données par la fonction
Indices Valeurs Jours
0 True Lundi
1 False Mardi
2 False Mercredi
3 False Jeudi
4 True Vendredi
5 False Samedi
6 False Dimanche

Avec l'indice du tableau qui représente l'exposant de la valeur. Il devient simple de retrouver la valeur correspondante à l'aide de la fonction "ValueByExposant(...)" écrite plus haut.

Composer un nombre

Nous pouvons modifier le tableau reçu à l'étape précédente.

mySelection(2) = True ' Sélectionne le mercredi, j'ai maintenant une nouvelle valeur dans mon nombre.

Dans ce cas, le nombre n'est plus valide et il faut le reconstruire.

La fonction suivante attend un tableau de booléens en paramètres et retourne la valeur des constantes sélectionnées (Long).

Function Encode(ByRef argBoolean() As Boolean) As Long

  Dim i As Long, myNumber As Long

  myNumber = 0

  For i = LBound(argBoolean) To UBound(argBoolean)

    If (argBoolean(i)) Then

      myNumber = myNumber + ValueByExposant(i) ' Convertir l'exposant en nombre.

    End If

  Next i

  Encode = myNumber

End Function

Appel de la fonction

Dim MonJour As Long

MonJour = Encode(mySelection) ' Passage du tableau de booléens.

MsgBox MonJour

La variable MonJour prend la valeur 21.

Dans le tableau Lundi et vendredi sont déjà cochés ce qui donne 17. Après l’ajout du mercredi de valeur 4, nous obtenons bien 21.

Un exemple concret

Gérer des choix multiples

Nous allons, gérer des cases à cocher (checkbox) sur un formulaire à l'aide de notre énumération.

Il faut pouvoir sélectionner des jours qui doivent être convertis en nombre et si nous passons un nombre que les bonnes cases soient cochées.

Formulaire VBA gérant des cases à cocher (checkbox)
Figure 3 : Formulaire exemple de choix multiples.

Nommer les cases à cocher

La façon de nommer les checkbox a son importance et elles répondent toutes au même schéma, à savoir :

  • Un libellé
    Il sera commun à toutes les cases à cocher par exemple "chkJour".
  • Un indice
    Il est concaténé à la suite du libellé.

La première checkbox porte l'indice 0.

J'ai ainsi les noms "chkJour0" qui représente ESemaine.eLundi, "chkJour1", "chkJour2",..., jusqu'à "chkJour6" pour ESemaine.eDimanche.

Si vous fait attention, cet indice n'est ni plus ni moins que la puissance de nos constantes.

Attribuer les "Captions" aux contrôles lors de l'initialisation

Il est possible de manipuler un contrôle par son nom avec l'instruction :

Me.Controls("Nom_Control").Caption = "..."

Reprenons notre tableau de chaines :

MesJours = Array("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")

L'indice de l'array est aussi l'indice de la checkbox. Il suffit donc de reconstituer le nom du contrôle avec l'indice du tableau.

Cette fonction doit être dans l'UserForm. Le mieux étant de l'appeler sur le Initialize().

Private Sub UserForm_Initialize()

  ' Définir les "Captions" des cases à cocher.

  Dim i As Long

  For i = LBound(MesJours) To UBound(MesJours)

    Me.Controls("chkJour" & i).Caption = MesJours(i) ' Affectation du "Caption".

  Next

End Sub

Cocher les bonnes cases

Il faut ensuite cocher les cases dont le jour est dans notre énumération.

C'est la même logique que la procédure précédente sauf que nous affectons la propriété "Value".

Nous écrivons dans le formulaire une fonction qui appellera la fonction "Decode(...)" vue précédemment.

Nous récupérons donc un tableau de booléens, il est alors facile d'attribuer aux coches la bonne valeur.

Private Sub DecodeDay()

  Dim i As Long

  ' Tranformer la valeur de l'attribut en booléen.
  ' Pour savoir s'il faut cocher ou non selon que l'attribut existe ou pas dans la portée.
  ' Contient l'état des constantes composant le nombre.

  Dim myCoche() As Boolean

  myCoche = Decode(ESemaine.[_Last], m_DateSample) ' m_DateSample = le nombre à décoder.

  For i = LBound(myCoche) To UBound(myCoche) - 1

    ' Reconstruire le nom de la coche à affecter.
    ' Affecter la propriété "Checked" selon que l'élément est dans notre nombre ou pas.

    Me.Controls("chkJour" & i).Value = myCoche(i)

  Next

End Sub

Exempe d'appel sur UserForm_Initialize()

Private m_DateSample As ESemaine

Private Sub UserForm_Initialize()

  m_DateSample = ESemaine.eLundi Or ESemaine.eDimanche Or ESemaine.eMercredi Or ESemaine.eMardi

  ' Procédure à appeler chaque fois que la valeur de l'énumération m_DateSample change.

  DecodeDay

Convertir les coches en valeurs de l'énumération

Si les cases "Lundi" et "Mardi" sont cochées, il faut attribuer la valeur 3 au nombre sous-jacent aux coches, c'est donc l'addition des indices des cases qui sont cochées.

Un tableau de booléens de taille égale aux nombres de constantes de l'énumération est remplit avec les valeurs des coches. Il est ensuite transmis à la fonction "Encode()" écrite plus haut.

Voici la fonction dans l'UserForm.

Private Sub EncodeDay()

  Dim m_Count As Byte

  Dim m_Day() As Boolean, i As Long

  Dim m_Jour As ESemaine

  m_Count = CountConstant(ESemaine.[_Last])

  ReDim m_Day(m_Count) ' Prêt à recevoir les booléens.

  For i = LBound(m_Day) To UBound(m_Day) - 1 ' Parcourir les éléments de l'énumération.

    m_Day(i) = Me.Controls("chkJour" & i).Value ' Donner la valeur de la coche.

  Next i

  m_Jour = Encode(m_Day) ' Appelle la fonction de conversion.

  Me.txtDay.Text = m_Jour

  End Sub

Elle sera appelée chaque fois qu'une valeur de coche change.

Private Sub chkDay0_Click()

  EncodeDay

End Sub

Private Sub chkDay1_Click()

  EncodeDay

End Sub

Private Sub chkDay2_Click()

  EncodeDay

End Sub

Note

Rien n'empêche de générer dynamiquement les cases à cocher sur le même principe, nous avons ainsi un système automatique à choix multiple qui peut être inclus dans n'importe quel projet.

Mise en place d'un exemple d'utilisation

Dans le formulaire du classeur de démonstration, vous trouverez une ComboBox cbxDayType défini ainsi :

Private Sub UserForm_Initialize()

  ' Quelques valeurs exemples pour manipuler l'énumération.

  cbxDayType.AddItem "Aucun jour"

  cbxDayType.AddItem "Tous les jours"

  cbxDayType.AddItem "Jours de travail"

  cbxDayType.AddItem "Week-end"

Sur l'évènement Change() de la ComboBox, nous définissons le nombre correspondant à l'item choisi :

Private Sub cbxDayType_Change()

  ' Définir des valeurs pour notre énumération.

  Select Case Me.cbxDayType.ListIndex

    Case 0 ' Aucun jour.

      m_DateSample = ESemaine.[_First]

    Case 1 ' Tous les jours.

      m_DateSample = ESemaine.eLundi Or ESemaine.eDimanche Or ESemaine.eMercredi Or ESemaine.eMardi Or ESemaine.eSamedi Or ESemaine.eJeudi Or ESemaine.eVendredi

    Case 2 ' Jour ouvrés.

      m_DateSample = ESemaine.eLundi Or ESemaine.eMercredi Or ESemaine.eMardi Or ESemaine.eJeudi Or ESemaine.eVendredi

    Case 3 ' Week-end.

      m_DateSample = ESemaine.eDimanche Or ESemaine.eSamedi

    Case Else

      MsgBox "Je n'ai pas compris votre demande !", vbInformation Or vbOKOnly

    End Select ' Me.cbxDayType.ListIndex

  DecodeDay ' Rafraichir les cases à cocher.

End Sub

Quand la valeur de la ComboBox change, une valeur est attribuée à l'énumération et les coches sont attribuées aux CheckBox.

Complément

Énumérations complexes

Rien n'empêche d'avoir des énumérations plus évoluées, et mélanger allègrement suite incrémentale et puissance :

Enum UneEnum

  [_First]

  eConstA = 1 ' Énumération incrémentale.

  eConstB ' 2.

  eConstC ' 3.

  eConstD = 0 ' Indique un nouveau groupe de valeurs.

  eConstE = 8 ' On part sur des puissances.

  eConstF = 16

  eConstG = 256 ' Ici on laisse un trou.

  eConstH = 0 ' Création d'un nouveau groupe de valeurs.

  eConstI = 512

  [_Last]

End Enum

Si vous allez voir dans l'aide Visual Basic la fonction MsgBox, vous retrouverez le même arrangement pour les constantes.

Les énumérations offrent de belles perspectives

Les fonctions pour les manipuler sont simples, courtes et facilement identifiables.

Comme il n'y a pas de contrôle de type de la part de VB, ce n'est pas compliqué de créer une classe de base qui reçoit et retourne des long pour gérer les énumérations.

Les usages sont nombreux puisque les énumérations gèrent des choix, ce qui est un besoin primaire de tout programme informatique.

Limites des énumérations

En extrapolant, nous pouvons dire qu'une énumération est un moyen de stocker de l'information comme peut le faire un champ dans une base de données.

Je peux imaginer un champ d'une table Microsoft Access ou d’un tableau Microsoft Excel qui conserve les valeurs combinées de l'énumération.

Exemple d'une table stockant des énumérations
T_PRESENCE
CONTACT_ID CONTACT_NOM CONTACT_PRESENCE
1 Toto 20
2 Titi 1
3 Tata 45

Dans ce cas, j'économise la table du côté "plusieurs". En contrepartie, les données sont masquées ce qui est loin d'être conseillé.
L'accès aux données ne devrait pas dépendre d'un processus externe (code – formulaire).

Téléchargement

Télécharger le classeur exemple Énumérations en VBA (Excel 2010).


Achetez mon ouvrage !

Mon PDF « Créer un planning perpétuel sur Microsoft Excel sans macro » est disponible à la vente.

Pour plus d’informations, rendez-vous sur la page dédiée.