Eklablog Tous les blogs
Editer l'article Suivre ce blog Administration + Créer mon blog
MENU

Blog traitant d'informatique, de programmation, de python, de golang, de linux...

Publicité

gtKonvert : 1ère application en Go avec GTK

1/ Organisation des fichiers du projet :

Ce projet contient 4 fichiers :

- gtKonvert.go est le fichier qui contient le "main".

- gtKonvert.glade est le fichier "xml" généré par Glade. Pour créer la partie visible, l'IHM avec ses boutons, labels, champs d'éditions, etc... la solution la plus simple est l'utilisation du logiciel Glade. Glade permet de construire une IHM Gtk graphiquement. Ce logiciel demande une petite prise en main, c'est un peu différent de Qt Designer par exemple, mais ça se fait très bien. Glade va sauver votre IHM au format XML, avec une extension "*.glade". Nous importerons ensuite ce fichier dans l'application gtKonvert, ce qui évitera d'avoir à créer toute cette IHM par programmation.

- gtkgui.go contient toute la partie "comportement" de l'IHM au sein de l'application gtKonvert : que ce passe-t'il quand on modifie telle valeur dans tel champ d'édition...

- units.go contient toutes les fonctions de conversion d'une unité vers une autre.

En Go, les fichiers sont organisés en packages. La structure de répertoires à utiliser quand on a plusieurs fichiers/packages dans un projet est de créer des sous dossiers dans le dossier principal. Chaque sous dossier portera le même nom que le package qu'il contient.

Ici, nous aurons donc :

- gtKonvert/gtKonvert.go : gtKonvert est le dossier racine de notre projet. gtKonvert.go est le fichier du package "main"

- gtKonvert/gtKonvert.glade : gtKonvert.glade pourrait être placé n'importe où, ça n'est pas un package. J'ai choisi de le laisser dans le répertoire racine.

- gtkgu/gtkguii.go : le fichier gtkgui.go est le fichier du package gtkgui.

- units/units.go : le fichier units.go est le fichier du package units.

 

2/ Le fichier gtKonvert.go :

    1 //
    2 // gtKonvert.go est une application avec IHM en GTK permettant
    3 // d'effectuer des conversions d'unités
    4 //
    5 // Version 2022.03.16
    6 //
    7 
    8 package main
    9 
   10 import (
   11 	"gtKonvert/gtkgui"
   12 	"log"
   13 	"os"
   14 
   15 	"github.com/gotk3/gotk3/gtk"
   16 )
   17 
   18 //
   19 //
   20 //
   21 //////////////////////// Variables globales
   22 //
   23 //
   24 
   25 //
   26 //
   27 //
   28 //////////////////////// Fonction Main
   29 //
   30 //
   31 func main() {
   32 	app, err := gtkgui.ApplicationNew("gtKonvert.com") // appID doit être comme un nom de domaine, en XXX.YYY
   33 	if err != nil {
   34 		log.Fatalln("Couldn't create app:", err)
   35 	}
   36 
   37 	// It looks like all builder code must execute in the context of `app`.
   38 	// If you try creating the builder inside the main function instead of
   39 	// the `app` "activate" callback, then you will get a segfault
   40 
   41 	app.Connect("activate", func() {
   42 		builder, err := gtkgui.BuilderNewFromFile("gtKonvert.glade")
   43 		if err != nil {
   44 			log.Fatalln("Couldn't make builder:", err)
   45 		}
   46 
   47 		// On récupère la MainWindow
   48 		obj, _ := builder.GetObject("mainWin")
   49 		wnd := obj.(*gtk.Window)
   50 		wnd.ShowAll()
   51 		app.AddWindow(wnd)
   52 	})
   53 	app.Run(os.Args)
   54 }

 

Ce fichier est le fichier principal, le package "main" qui contient la fonction 'main' à partir de laquelle l'application sera lancée. J'ai laissé volontairement beaucoup de commentaires à l'intérieur. J'ai créé ce fichier en m'inspirant d'exemples glanés ça et là sur le net et certains de ces commentaires d'origines me semblent vraiment importants.

 

Déroulement du programme :

En 1er lieu, on va dans la section import, déclarer tous les packages nécessaires au bon fonctionnement de l'application.

On va vraiment "créer" l'application GTK en ligne 32, et récupérer dans app un pointeur vers cette application GTK. Au passage, on va aussi récupérer un pointeur err vers une erreur si les choses se sont mal passées. Dans ce cas, la ligne 44 (log.Fatalln(....)) permettra de sortir de l'application en renvoyant le code d'erreur.

Revenons à la ligne 32, on doit passer un "appID" à la fonction gtkgui.ApplicationNew(). Cet "appID" est une chaine qui doit au moins contenir un point (comme un nom de domaine) : le site "gtKonvert.com" n'existe pas, mais passer "gtKonvert" renvoyait une erreur.

Ligne 41 : je n'ai (peut-être?) pas encore bien saisi le sens de cette ligne. Ce que j'en comprend est qu'au moment où l'application gtk va être activée, on va exécuter tout ce qui se trouve dans la fonction func() entre les lignes 41 à 52. app.Connect("activate, func(){}) va faire le lien entre le contenu de func(){} au moment du "activate"

Dans cette section, ligne 41 à 52, la 1ère étape est de créer l'IHM. Ici, on ne va pas la créer par programmation, mais charger le fichier glade qui contient toute la définition de cette IHM. Ce chargement est effectué ligne 42 par l'appel à la fonction BuilderNewFromFile() du package gtkgui (nous étudierons ce package en détail un peu plus tard). Cette fonction nous renvoie builder, un pointeur vers l'IHM qui nous permettra d'interragir avec tous ses composants (widgets), ainsi qu'un code d'erreur err au cas où l'opération n'a pas pu aboutir. Ensuite, soit on sort de l'application si une erreur est apparue, sinon, on va récupérer la fenêtre principale de l'IHM (qui contient tous les widgets), et l'ajouter à l'application gtk.

Si tout s'est bien passé, on peut alors démarrer l'application avec app.Run(os.Args).

 

3/ Le fichier gkgui.go :

    1 //
    2 //
    3 // gtkgui.go contient les fonctions permettant la gestion de l'IHM
    4 //
    5 // Version 2022.03.21
    6 //
    7 
    8 package gtkgui
    9 
   10 import (
   11 	"fmt"
   12 	"gtKonvert/units"
   13 	"regexp"
   14 	"strconv"
   15 	"strings"
   16 
   17 	"github.com/gotk3/gotk3/glib"
   18 	"github.com/gotk3/gotk3/gtk"
   19 )
   20 
   21 //
   22 //
   23 //
   24 //////////////////////// Variables globales
   25 //
   26 //
   27 var guiBuilder *gtk.Builder
   28 
   29 var appDegEntry, appRadEntry, appGradeEntry *gtk.Entry
   30 var degEntryHasFocus bool = true
   31 var radEntryHasFocus bool = false
   32 var gradeEntryHasFocus bool = false
   33 
   34 var appDegDecEntry, appDmsDecEntry, appDmDecEntry *gtk.Entry
   35 var degDecEntryHasFocus bool = true
   36 var dmsDecEntryHasFocus bool = false
   37 var dmDecEntryHasFocus bool = false
   38 
   39 var appKmEntry, appMilesEntry, appNauticEntry *gtk.Entry
   40 var kmEntryHasFocus bool = true
   41 var milesEntryHasFocus bool = false
   42 var nauticEntryHasFocus bool = false
   43 
   44 var appMsecEntry, appKmhEntry, appMphEntry, appKtEntry *gtk.Entry
   45 var msEntryHasFocus bool = true
   46 var kmhEntryHasFocus bool = true
   47 var mphEntryHasFocus bool = false
   48 var ktEntryHasFocus bool = false
   49 
   50 var signals = map[string]interface{}{ // Connexions signaux/Slots ici
   51 
   52 	"on_degEntry_grab_focus":   degEntryFocus,
   53 	"on_radEntry_grab_focus":   radEntryFocus,
   54 	"on_gradeEntry_grab_focus": gradeEntryFocus,
   55 	"on_degEntry_changed":      degEntryChanged,
   56 	"on_radEntry_changed":      radEntryChanged,
   57 	"on_gradeEntry_changed":    gradeEntryChanged,
   58 
   59 	"on_degDecEntry_grab_focus": degDecEntryFocus,
   60 	"on_dmsDecEntry_grab_focus": dmsDecEntryFocus,
   61 	"on_dmDecEntry_grab_focus":  dmDecEntryFocus,
   62 	"on_degDecEntry_changed":    degDecEntryChanged,
   63 	"on_dmsDecEntry_changed":    dmsDecEntryChanged,
   64 	"on_dmDecEntry_changed":     dmDecEntryChanged,
   65 
   66 	"on_kmEntry_grab_focus":     kmEntryFocus,
   67 	"on_milesEntry_grab_focus":  milesEntryFocus,
   68 	"on_nauticEntry_grab_focus": nauticEntryFocus,
   69 	"on_kmEntry_changed":        kmEntryChanged,
   70 	"on_milesEntry_changed":     milesEntryChanged,
   71 	"on_nauticEntry_changed":    nauticEntryChanged,
   72 
   73 	"on_msEntry_grab_focus":  msecEntryFocus,
   74 	"on_kmhEntry_grab_focus": kmhEntryFocus,
   75 	"on_mphEntry_grab_focus": mphEntryFocus,
   76 	"on_ktEntry_grab_focus":  ktEntryFocus,
   77 	"on_msEntry_changed":     msecEntryChanged,
   78 	"on_kmhEntry_changed":    kmhEntryChanged,
   79 	"on_mphEntry_changed":    mphEntryChanged,
   80 	"on_ktEntry_changed":     ktEntryChanged,
   81 }
   82 
   83 //
   84 //
   85 //
   86 //////////////////////// Création d'une nouvelle gtk.Application
   87 //
   88 //
   89 func ApplicationNew(appID string) (*gtk.Application, error) {
   90 	app, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE)
   91 	return app, err
   92 }
   93 
   94 // Création de la GUI
   95 //
   96 func BuilderNewFromFile(gladeFilename string) (*gtk.Builder, error) {
   97 	builder, err := gtk.BuilderNewFromFile(gladeFilename)
   98 	if err == nil {
   99 		guiBuilder = builder
  100 		ConnectSignals()
  101 		getWidgets()
  102 	}
  103 	return builder, err
  104 }
  105 
  106 // On établit la connexion des signaux => Les connexions sont définies dans la map "signals"
  107 //
  108 func ConnectSignals() {
  109 	guiBuilder.ConnectSignals(signals)
  110 }
  111 
  112 // On récupère ici les pointeurs vers les widgets pour pouvoir interragir avec (pointeurs app...)
  113 //
  114 func getWidgets() {
  115 	// On récupère les entry de conversions angulaires
  116 	//
  117 	obj, _ := guiBuilder.GetObject("degEntry")
  118 	degEntry, _ := obj.(*gtk.Entry)
  119 	appDegEntry = degEntry
  120 	obj, _ = guiBuilder.GetObject("radEntry")
  121 	radEntry, _ := obj.(*gtk.Entry)
  122 	appRadEntry = radEntry
  123 	obj, _ = guiBuilder.GetObject("gradeEntry")
  124 	gradeEntry, _ := obj.(*gtk.Entry)
  125 	appGradeEntry = gradeEntry
  126 
  127 	// On récupère les entry de conversion Lat/Lon
  128 	//
  129 	obj, _ = guiBuilder.GetObject("degDecEntry")
  130 	degDecEntry, _ := obj.(*gtk.Entry)
  131 	appDegDecEntry = degDecEntry
  132 	obj, _ = guiBuilder.GetObject("dmsDecEntry")
  133 	dmsDecEntry, _ := obj.(*gtk.Entry)
  134 	appDmsDecEntry = dmsDecEntry
  135 	obj, _ = guiBuilder.GetObject("dmDecEntry")
  136 	dmDecEntry, _ := obj.(*gtk.Entry)
  137 	appDmDecEntry = dmDecEntry
  138 
  139 	// On récupère les entry de conversions de longueurs/distances
  140 	//
  141 	obj, _ = guiBuilder.GetObject("kmEntry")
  142 	kmEntry, _ := obj.(*gtk.Entry)
  143 	appKmEntry = kmEntry
  144 	obj, _ = guiBuilder.GetObject("milesEntry")
  145 	milesEntry, _ := obj.(*gtk.Entry)
  146 	appMilesEntry = milesEntry
  147 	obj, _ = guiBuilder.GetObject("nauticEntry")
  148 	nauticEntry, _ := obj.(*gtk.Entry)
  149 	appNauticEntry = nauticEntry
  150 
  151 	// On récupère les entry de conversions de vitesses
  152 	//
  153 	obj, _ = guiBuilder.GetObject("msEntry")
  154 	msecEntry, _ := obj.(*gtk.Entry)
  155 	appMsecEntry = msecEntry
  156 	obj, _ = guiBuilder.GetObject("kmhEntry")
  157 	kmhEntry, _ := obj.(*gtk.Entry)
  158 	appKmhEntry = kmhEntry
  159 	obj, _ = guiBuilder.GetObject("mphEntry")
  160 	mphEntry, _ := obj.(*gtk.Entry)
  161 	appMphEntry = mphEntry
  162 	obj, _ = guiBuilder.GetObject("ktEntry")
  163 	ktEntry, _ := obj.(*gtk.Entry)
  164 	appKtEntry = ktEntry
  165 }
  166 
  167 //
  168 //
  169 //
  170 //////////////////////// Conversions - Fonctions communes
  171 //
  172 //
  173 func showGtkDialogWarning(entryWidget *gtk.Entry) {
  174 	entryWidget.SetIconFromIconName(gtk.ENTRY_ICON_PRIMARY, "gtk-dialog-warning")
  175 	entryWidget.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "gtk-dialog-warning")
  176 }
  177 func getTextEntryFloat64(entryWidget *gtk.Entry) (float64, error) {
  178 	str, _ := entryWidget.GetText()
  179 	return strconv.ParseFloat(str, 64)
  180 }
  181 func clearIcons(entries []*gtk.Entry) {
  182 	for _, element := range entries {
  183 		element.SetIconFromIconName(gtk.ENTRY_ICON_PRIMARY, "")
  184 		element.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "")
  185 	}
  186 }
  187 func printFloat64(entries []*gtk.Entry, inValue float64, funks []func(float64) float64) {
  188 	// printFloat64 prend en entrée une slice de gtk.Entry ainsi qu'une slice de fonctions associés à chacune des gtk.entry
  189 	// On peut voir funk un peu comme une liste de pointeurs vers des fonctions
  190 	for index, element := range entries {
  191 		element.SetText(fmt.Sprintf("%g", funks[index](inValue)))
  192 	}
  193 }
  194 
  195 //
  196 //
  197 //
  198 //////////////////////// Conversions Deg/Rad/Grade - Fonctions de sélection de l'entry sélectionnée
  199 //
  200 //
  201 func degEntryFocus() {
  202 	degEntryHasFocus, radEntryHasFocus, gradeEntryHasFocus = true, false, false
  203 }
  204 func radEntryFocus() {
  205 	degEntryHasFocus, radEntryHasFocus, gradeEntryHasFocus = false, true, false
  206 }
  207 func gradeEntryFocus() {
  208 	degEntryHasFocus, radEntryHasFocus, gradeEntryHasFocus = false, false, true
  209 }
  210 
  211 ////////////////////////  Conversions Deg/Rad/Grade - Appels aux fonctions de conversion et d'affichage
  212 //
  213 func degEntryChanged() {
  214 	if degEntryHasFocus {
  215 		value64, err := getTextEntryFloat64(appDegEntry)
  216 		if err == nil {
  217 			clearIcons([]*gtk.Entry{appDegEntry, appRadEntry, appGradeEntry})
  218 			printFloat64([]*gtk.Entry{appRadEntry, appGradeEntry}, value64, []func(float64) float64{units.DegToRad, units.DegToGrade})
  219 		} else {
  220 			showGtkDialogWarning(appDegEntry)
  221 		}
  222 	}
  223 }
  224 func radEntryChanged() {
  225 	if radEntryHasFocus {
  226 		value64, err := getTextEntryFloat64(appRadEntry)
  227 		if err == nil {
  228 			clearIcons([]*gtk.Entry{appDegEntry, appRadEntry, appGradeEntry})
  229 			printFloat64([]*gtk.Entry{appDegEntry, appGradeEntry}, value64, []func(float64) float64{units.RadToDeg, units.RadToGrade})
  230 		} else {
  231 			showGtkDialogWarning(appRadEntry)
  232 		}
  233 	}
  234 }
  235 func gradeEntryChanged() {
  236 	if gradeEntryHasFocus {
  237 		value64, err := getTextEntryFloat64(appGradeEntry)
  238 		if err == nil {
  239 			clearIcons([]*gtk.Entry{appDegEntry, appRadEntry, appGradeEntry})
  240 			printFloat64([]*gtk.Entry{appDegEntry, appRadEntry}, value64, []func(float64) float64{units.GradeToDeg, units.GradeToRad})
  241 		} else {
  242 			showGtkDialogWarning(appGradeEntry)
  243 		}
  244 	}
  245 }
  246 
  247 //
  248 //
  249 //
  250 //////////////////////// Conversions Lat/Lon - Fonctions de sélection de l'entry sélectionnée
  251 //
  252 //
  253 func degDecEntryFocus() {
  254 	degDecEntryHasFocus, dmsDecEntryHasFocus, dmDecEntryHasFocus = true, false, false
  255 }
  256 func dmsDecEntryFocus() {
  257 	degDecEntryHasFocus, dmsDecEntryHasFocus, dmDecEntryHasFocus = false, true, false
  258 }
  259 func dmDecEntryFocus() {
  260 	degDecEntryHasFocus, dmsDecEntryHasFocus, dmDecEntryHasFocus = false, false, true
  261 }
  262 
  263 ////////////////////////  Conversions Lat/Lon - Appels aux fonctions de conversion et d'affichage
  264 //
  265 func degDecEntryChanged() {
  266 	if degDecEntryHasFocus {
  267 		value64, err := getTextEntryFloat64(appDegDecEntry)
  268 		if err == nil {
  269 			clearIcons([]*gtk.Entry{appDegDecEntry, appDmsDecEntry, appDmDecEntry})
  270 			appDmsDecEntry.SetText(units.DegToDMSDec(value64).Str)
  271 			appDmDecEntry.SetText(units.DegToDMDec(value64).Str)
  272 		} else {
  273 			showGtkDialogWarning(appDegDecEntry)
  274 		}
  275 	}
  276 }
  277 func dmsDecEntryChanged() {
  278 	if dmsDecEntryHasFocus {
  279 		badEntry := true
  280 		var degVar int64
  281 		var minVar float64
  282 		var secVar float64
  283 		str, _ := appDmsDecEntry.GetText()
  284 		str = strings.ToUpper(str)
  285 		regexDeg := `^[-NSEW]?\d{1,3}`
  286 		regexMin := `[°][\s]\d{1,2}(\.?)\d*'`
  287 		regexSecD := `[\s]\d{1,2}(\.?)\d*'?$`
  288 		degMatched, _ := regexp.MatchString(regexDeg, str)
  289 		minMatched, _ := regexp.MatchString(regexMin, str)
  290 		secMatched, _ := regexp.MatchString(regexSecD, str)
  291 		if degMatched && minMatched && secMatched {
  292 			sliceStr := strings.Split(str, "° ")
  293 			if len(sliceStr) == 2 {
  294 				minSecSlice := strings.Split(sliceStr[1], "' ") //On récupère les minutes dans minSecSlice[0] et les secondes dans minSecSlice[1]
  295 				if len(minSecSlice) == 2 {
  296 					mindecValue, _ := strconv.ParseInt(minSecSlice[0], 0, 64)
  297 					if mindecValue < 60 {
  298 						minVar = float64(mindecValue)
  299 					}
  300 					theLen := len(minSecSlice[1])
  301 					strFinal := minSecSlice[1][theLen-1:]
  302 					if strFinal == "\"" {
  303 						minSecSlice[1] = minSecSlice[1][0 : len(minSecSlice[1])-1] // Ici, on supprime le " final si besoin
  304 					}
  305 					secdecValue, _ := strconv.ParseFloat(minSecSlice[1], 64)
  306 					if secdecValue < 60.0 {
  307 						secVar = secdecValue
  308 						strSign := "+"
  309 						strDeg := sliceStr[0][1:]
  310 						regexSign := `^[-NSEW]`
  311 						degMatched, _ = regexp.MatchString(regexSign, str)
  312 						if degMatched { //On a une indication [-NSEW] en début de chaine
  313 							strSign = sliceStr[0][0:1]
  314 							if strSign == "N" || strSign == "W" || strSign == "-" {
  315 								strSign = "-"
  316 							} else {
  317 								strSign = "+"
  318 							}
  319 						} else { // On n'a aucune indication en début de chaine, on a affaire à une valeur de degrés positive
  320 							strSign = "+"
  321 							strDeg = sliceStr[0][0:]
  322 						}
  323 						strDeg = strSign + strDeg
  324 						degTmp, _ := strconv.ParseInt(strDeg, 0, 64)
  325 						degVar = degTmp
  326 						badEntry = false
  327 					}
  328 				}
  329 			}
  330 		}
  331 		if !badEntry {
  332 			clearIcons([]*gtk.Entry{appDegDecEntry, appDmsDecEntry, appDmDecEntry})
  333 			var inVar units.DegDMS
  334 			inVar.D = int(degVar)
  335 			inVar.M = minVar
  336 			inVar.S = secVar
  337 			appDegDecEntry.SetText(fmt.Sprintf("%g", units.DMSDecToDeg(inVar)))
  338 			appDmDecEntry.SetText(units.DMSDecToDMDec(inVar).Str)
  339 		} else {
  340 			showGtkDialogWarning(appDmsDecEntry)
  341 		}
  342 	}
  343 }
  344 func dmDecEntryChanged() {
  345 	if dmDecEntryHasFocus {
  346 		badEntry := true
  347 		var degVar int64
  348 		var minVar float64
  349 		str, _ := appDmDecEntry.GetText()
  350 		str = strings.ToUpper(str)
  351 		regexDeg := `^[-NSEW]?\d{1,3}`
  352 		regexMinD := `[°][\s]\d{1,2}(\.?)\d*'?$`
  353 		degMatched, _ := regexp.MatchString(regexDeg, str)
  354 		minMatched, _ := regexp.MatchString(regexMinD, str)
  355 		if degMatched && minMatched {
  356 			sliceStr := strings.Split(str, "° ")
  357 			if len(sliceStr) == 2 {
  358 				theLen := len(sliceStr[1])
  359 				strFinal := sliceStr[1][theLen-1:]
  360 				if strFinal == "'" {
  361 					sliceStr[1] = sliceStr[1][0 : len(sliceStr[1])-1] // Ici, on supprime le ' final si besoin
  362 				}
  363 				mindecValue, _ := strconv.ParseFloat(sliceStr[1], 64)
  364 				if mindecValue < 60.0 {
  365 					minVar = mindecValue
  366 					strSign := "+"
  367 					strDeg := sliceStr[0][1:]
  368 					regexSign := `^[-NSEW]`
  369 					degMatched, _ = regexp.MatchString(regexSign, str)
  370 					if degMatched { //On a une indication [-NSEW] en début de chaine
  371 						strSign = sliceStr[0][0:1]
  372 						if strSign == "N" || strSign == "W" || strSign == "-" {
  373 							strSign = "-"
  374 						} else {
  375 							strSign = "+"
  376 						}
  377 					} else { // On n'a aucune indication en début de chaine, on a affaire à une valeur de degrés positive
  378 						strSign = "+"
  379 						strDeg = sliceStr[0][0:]
  380 					}
  381 					strDeg = strSign + strDeg
  382 					degTmp, _ := strconv.ParseInt(strDeg, 0, 64)
  383 					degVar = degTmp
  384 					badEntry = false
  385 				}
  386 			}
  387 		}
  388 		if !badEntry {
  389 			clearIcons([]*gtk.Entry{appDegDecEntry, appDmsDecEntry, appDmDecEntry})
  390 			var inVar units.DegDMS
  391 			inVar.D = int(degVar)
  392 			inVar.M = minVar
  393 			inVar.S = 0.0
  394 			appDegDecEntry.SetText(fmt.Sprintf("%g", units.DMDecToDeg(inVar)))
  395 			appDmsDecEntry.SetText(units.DMDecToDMSDec(inVar).Str)
  396 		} else {
  397 			showGtkDialogWarning(appDmDecEntry)
  398 		}
  399 	}
  400 }
  401 
  402 //
  403 //
  404 //
  405 //////////////////////// Conversions Longueurs/Distances - Fonctions de sélection de l'entry sélectionnée
  406 //
  407 //
  408 func kmEntryFocus() {
  409 	kmEntryHasFocus, milesEntryHasFocus, nauticEntryHasFocus = true, false, false
  410 }
  411 func milesEntryFocus() {
  412 	kmEntryHasFocus, milesEntryHasFocus, nauticEntryHasFocus = false, true, false
  413 }
  414 func nauticEntryFocus() {
  415 	kmEntryHasFocus, milesEntryHasFocus, nauticEntryHasFocus = false, false, true
  416 }
  417 
  418 ////////////////////////  Conversions Longueurs/Distances - Appels aux fonctions de conversion et d'affichage
  419 //
  420 func kmEntryChanged() { // Rajouter l'unité Feet !
  421 	if kmEntryHasFocus {
  422 		value64, err := getTextEntryFloat64(appKmEntry)
  423 		if err == nil {
  424 			clearIcons([]*gtk.Entry{appKmEntry, appMilesEntry, appNauticEntry})
  425 			printFloat64([]*gtk.Entry{appMilesEntry, appNauticEntry}, value64, []func(float64) float64{units.KmToMiles, units.KmToNautics})
  426 		} else {
  427 			showGtkDialogWarning(appKmEntry)
  428 		}
  429 	}
  430 }
  431 func milesEntryChanged() {
  432 	if milesEntryHasFocus {
  433 		value64, err := getTextEntryFloat64(appMilesEntry)
  434 		if err == nil {
  435 			clearIcons([]*gtk.Entry{appKmEntry, appMilesEntry, appNauticEntry})
  436 			printFloat64([]*gtk.Entry{appKmEntry, appNauticEntry}, value64, []func(float64) float64{units.MilesToKm, units.MilesToNautics})
  437 		} else {
  438 			showGtkDialogWarning(appMilesEntry)
  439 		}
  440 	}
  441 }
  442 func nauticEntryChanged() {
  443 	if nauticEntryHasFocus {
  444 		value64, err := getTextEntryFloat64(appNauticEntry)
  445 		if err == nil {
  446 			clearIcons([]*gtk.Entry{appKmEntry, appMilesEntry, appNauticEntry})
  447 			printFloat64([]*gtk.Entry{appKmEntry, appMilesEntry}, value64, []func(float64) float64{units.NauticsToKm, units.NauticsToMiles})
  448 		} else {
  449 			showGtkDialogWarning(appNauticEntry)
  450 		}
  451 	}
  452 }
  453 
  454 //
  455 //
  456 //
  457 //////////////////////// Conversions Vitesses - Fonctions de sélection de l'entry sélectionnée
  458 //
  459 //
  460 func msecEntryFocus() {
  461 	msEntryHasFocus, kmhEntryHasFocus, mphEntryHasFocus, ktEntryHasFocus = true, false, false, false
  462 }
  463 func kmhEntryFocus() {
  464 	msEntryHasFocus, kmhEntryHasFocus, mphEntryHasFocus, ktEntryHasFocus = false, true, false, false
  465 }
  466 func mphEntryFocus() {
  467 	msEntryHasFocus, kmhEntryHasFocus, mphEntryHasFocus, ktEntryHasFocus = false, false, true, false
  468 }
  469 func ktEntryFocus() {
  470 	msEntryHasFocus, kmhEntryHasFocus, mphEntryHasFocus, ktEntryHasFocus = false, false, false, true
  471 }
  472 
  473 ////////////////////////  Conversions Vitesses - Appels aux fonctions de conversion et d'affichage
  474 //
  475 func msecEntryChanged() {
  476 	if msEntryHasFocus {
  477 		value64, err := getTextEntryFloat64(appMsecEntry)
  478 		if err == nil {
  479 			clearIcons([]*gtk.Entry{appMsecEntry, appKmhEntry, appMphEntry, appKtEntry})
  480 			printFloat64([]*gtk.Entry{appKmhEntry, appMphEntry, appKtEntry}, value64, []func(float64) float64{units.MsecToKmh, units.MsecToMph, units.MsecToKt})
  481 		} else {
  482 			showGtkDialogWarning(appMsecEntry)
  483 		}
  484 	}
  485 }
  486 func kmhEntryChanged() {
  487 	if kmhEntryHasFocus {
  488 		value64, err := getTextEntryFloat64(appKmhEntry)
  489 		if err == nil {
  490 			clearIcons([]*gtk.Entry{appMsecEntry, appKmhEntry, appMphEntry, appKtEntry})
  491 			printFloat64([]*gtk.Entry{appMsecEntry, appMphEntry, appKtEntry}, value64, []func(float64) float64{units.KmhToMsec, units.KmhToMph, units.KmhToKt})
  492 		} else {
  493 			showGtkDialogWarning(appKmhEntry)
  494 		}
  495 	}
  496 }
  497 func mphEntryChanged() {
  498 	if mphEntryHasFocus {
  499 		value64, err := getTextEntryFloat64(appMphEntry)
  500 		if err == nil {
  501 			clearIcons([]*gtk.Entry{appMsecEntry, appKmhEntry, appMphEntry, appKtEntry})
  502 			printFloat64([]*gtk.Entry{appMsecEntry, appKmhEntry, appKtEntry}, value64, []func(float64) float64{units.MphToMsec, units.MphToKmh, units.MphToKt})
  503 		} else {
  504 			showGtkDialogWarning(appMphEntry)
  505 		}
  506 	}
  507 }
  508 func ktEntryChanged() {
  509 	if ktEntryHasFocus {
  510 		value64, err := getTextEntryFloat64(appKtEntry)
  511 		if err == nil {
  512 			clearIcons([]*gtk.Entry{appMsecEntry, appKmhEntry, appMphEntry, appKtEntry})
  513 			printFloat64([]*gtk.Entry{appMsecEntry, appKmhEntry, appMphEntry}, value64, []func(float64) float64{units.KtToMsec, units.KtToKmh, units.KtToMph})
  514 		} else {
  515 			showGtkDialogWarning(appKtEntry)
  516 		}
  517 	}
  518 }

 

Ce fichier est le plus conséquent du programme. Il contient toute la création de l'IHM GTK ainsi que son comportement.

On y trouve en 1er lieu la déclaration du package, ainsi que les imports. On notera que c'est ici que sera importé le package units qui lui sera vraiment en charge des conversions.

 

Déclaration des variables :

On trouve ensuite la déclaration de toute une liste de variables globales. Je voulais que cette application mette à jour les résultats des conversions en temps réel par rapport à l'entrée de l'utilisateur, pour ne pas avoir à attendre un appui sur [ENTER]. Pour cela, j'ai besoin de connaitre à chaque instant quel "entry widget" a le focus. Cela sera le rôle de tous les booléens déclarés ici.

On trouvera aussi pour chaque "entry widget" - de type gtk.Entry un pointeur vers celui-ci. Cela permettra de mettre à jour les résultats de conversions dans tous les "entry widgets" concernés.

Au début de cette liste de déclaration de variables, on a un pointeur vers le "gtk.Builder", que l'on récupèrera lors de la création de l'IHM, dans la fonction BuilderNewFromFile(), lignes 96/97. Ce pointeur nous permettra ensuite de faire le lien entre tous les widgets de l'IHM déclarés dans le fichier Glade, et toutes les variables du programme servant à manipuler ces widgets.

Enfin, pour en terminer avec les variables globales, on a la déclaration (ligne 50) d'une map qui va nous permettre de faire les liens entre les signaux émis par les widgets, et les fonctions auxquelles ces signaux devront être connectés. Pour chaque entrée, la clé de la map est une string qui correspond au nom du signal émis, tel que défini dans GTK : si on prend par exemple la 1ère entrée, "on_degEntry_grab_focus" correspond au nom du signal émis lorsque le widget nommé degEntry récupère le focus. Lorsque ce signal sera émis, la fonction degEntryFocus() (ligne 201) sera appelée. Pour "correspondre" à une fonction, la valeur associé à la clé dans cette map doit être de type interface{} (voir ligne 50) - on pourra assimiler (faute d'une meilleure compréhension pour le moment) une interface à une fonction.

A la suite de cette section définissant toutes les variables globales, on va trouver toutes les fonctions.

 

Fonctions de création de l'application, de l'IHM et d'initialisation :

Les fonctions de la 1ère section (ligne 86 à 165) servent à créer l'application et à initialiser toutes les variables nécessaires. La fonction ApplicationNew() est appelée à partir du "main", puis si tout s'est bien passé, "main" va appeler la fonction BuilderNewFromFile() qui va créer l'IHM à partir du fichier Glade.

La fonction BuilderNewFromFile() appelera la fonction de connections des signaux - ConnectSignal() - ainsi que la fonction getWidgets() qui initialisera les pointeurs (définis en variables globales) vers les widgets.

 

Fonctions communes utilisées par les fonctions de conversions :

Les fonctions de conversion fonctionnent toutes sur le même modèle. Si vous regardez les fonctions de la section de conversion deg/rad/grade (de la ligne 198 à la ligne 246), vous verrez qu'il y a un 1er ensemble de fonctions dont le nom se termine par Focus(), et un 2ème ensemble dont le nom se termine par Changed().

Les fonctions dont le nom se termine par Focus() sont appelés lorsqu'un entryWidget obtient le focus, ils mettent simplement des flags booléens à jour. Les fonctions dont le nom se termine par Changed() sont appelées lorsque la valeur dans l'entryWidget correspondant est éditée/modifiée. A part pour les conversions Lat/Lon, toutes les autres fonctions Changed() auront un comportement identique, à savoir :

- lecture de la valeur contenue par l'entryWidget

- vérification que cette valeur est bien de type float64

- mise à jour des icônes indicateurs d'erreur de format - pour le cas où la valeur entrée n'est pas convertible en float64

- appel aux fonctions de conversion d'unités si la valeur entrée est convertible

Pour les 4 process définis juste ci-dessus, les fonctions communes (définies entre les lignes 167 et 194) seront appelées par la fonction Changed() de l'entryWidget concerné. La fonction printFloat64() se verra aussi passer une liste de fonctions en paramètre. Ces fonctions passées sont directement celles qui devront être appelées dans le package units.

 Les fonctions de conversion Lat/Lon nécessitent un traitement un peu plus complexe. On s'assure du format entré par une vérification regex, puis on splitte la chaine entrée afin de s'assurer que les valeur minutes et secondes quand elles sont présentes respectent les limites imposées (<60). Si tout est OK, on peut alors faire appel aux fonctions de conversions situées dans  le package units.

 

4/ Le fichier units.go :

    1 //
    2 //
    3 // units.go contient des fonctions permettant des conversions d'unités
    4 //
    5 // Version 2022.03.20
    6 //
    7 
    8 package units
    9 
   10 import (
   11 	"fmt"
   12 )
   13 
   14 const (
   15 	gradeCoeff   float64 = 1.111111111111111111111111
   16 	f180         float64 = 180.0000000000000000000000
   17 	pi64         float64 = 3.1415926535897932384626433832795 // Plus précis que math.pi
   18 	milesCoeff   float64 = 1.609344000000000000000000
   19 	nauticsCoeff float64 = 1.852000000000000000000000
   20 )
   21 
   22 type DegDMS struct {
   23 	D   int
   24 	M   float64
   25 	S   float64
   26 	Str string
   27 }
   28 
   29 //
   30 //
   31 //
   32 //////////////////////// Conversions angulaires
   33 //
   34 //
   35 func DegToRad(deg float64) float64 {
   36 	return (deg * pi64 / f180)
   37 }
   38 
   39 func DegToGrade(deg float64) float64 {
   40 	return (deg * gradeCoeff)
   41 }
   42 
   43 func RadToDeg(rad float64) float64 {
   44 	return (rad * f180 / pi64)
   45 }
   46 
   47 func RadToGrade(rad float64) float64 {
   48 	return (gradeCoeff * rad * f180 / pi64)
   49 }
   50 
   51 func GradeToDeg(grade float64) float64 {
   52 	return (grade / gradeCoeff)
   53 }
   54 
   55 func GradeToRad(grade float64) float64 {
   56 	return (grade * pi64 / (f180 * gradeCoeff))
   57 }
   58 
   59 //
   60 //
   61 //
   62 //////////////////////// Conversions Lat/Lon
   63 //
   64 //
   65 func DegToDMSDec(deg float64) DegDMS { // Conversion Degré => DMS
   66 	var result DegDMS
   67 	result.D = int(deg)
   68 	minDeg := (deg - float64(result.D)) * 60
   69 	result.M = float64(int(minDeg))
   70 	minSec := (minDeg - float64(result.M)) * 60
   71 	result.S = minSec
   72 	strM, strS := "", ""
   73 	if result.M < 10 {
   74 		strM = "0" + strM
   75 	}
   76 	if result.S < 10 {
   77 		strS = "0" + strS
   78 	}
   79 	result.Str = fmt.Sprint(result.D) + "° " + strM + fmt.Sprint(result.M) + "' " + strS + fmt.Sprint(result.S) + "\""
   80 	if len(result.Str) > 18 {
   81 		result.Str = result.Str[0:17] + "\""
   82 	}
   83 	return result
   84 }
   85 func DegToDMDec(deg float64) DegDMS { // Conversion Degré => DM
   86 	var result DegDMS
   87 	result.D = int(deg)
   88 	minDeg := (deg - float64(result.D)) * 60
   89 	result.M = minDeg
   90 	result.S = 0
   91 	strM := ""
   92 	if result.M < 10 {
   93 		strM = "0" + strM
   94 	}
   95 	result.Str = fmt.Sprint(result.D) + "° " + strM + fmt.Sprint(result.M) + "'"
   96 	if len(result.Str) > 18 {
   97 		result.Str = result.Str[0:17] + "'"
   98 	}
   99 	return result
  100 }
  101 
  102 func DMSDecToDeg(DMSDec DegDMS) float64 { // Conversion DMS => Degré
  103 	var result float64 = 0
  104 	result += float64(DMSDec.D)
  105 	if result > 0.0 {
  106 		result += DMSDec.M / 60.0
  107 		result += DMSDec.S / 3600.0
  108 	} else {
  109 		result -= DMSDec.M / 60.0
  110 		result -= DMSDec.S / 3600.0
  111 	}
  112 	return result
  113 }
  114 func DMSDecToDMDec(DMSDec DegDMS) DegDMS { // Conversion DMS => DM
  115 	var result DegDMS
  116 	result.D = DMSDec.D
  117 	result.M = DMSDec.M + (DMSDec.S / 60.0)
  118 	result.S = 0.0
  119 	strM := ""
  120 	if result.M < 10 {
  121 		strM = "0" + strM
  122 	}
  123 	result.Str = fmt.Sprint(result.D) + "° " + strM + fmt.Sprint(result.M) + "'"
  124 	if len(result.Str) > 18 {
  125 		result.Str = result.Str[0:17] + "'"
  126 	}
  127 	return result
  128 }
  129 
  130 func DMDecToDeg(DMDec DegDMS) float64 { // Conversion DM => Degré
  131 	var result float64 = 0
  132 	result += float64(DMDec.D)
  133 	if result > 0.0 {
  134 		result += DMDec.M / 60.0
  135 	} else {
  136 		result -= DMDec.M / 60.0
  137 	}
  138 	return result
  139 }
  140 func DMDecToDMSDec(DMDec DegDMS) DegDMS { // Conversion DM => DMS
  141 	var result DegDMS
  142 	result.D = DMDec.D
  143 	result.M = float64(int(DMDec.M))
  144 	result.S = 60.0 * (DMDec.M - result.M)
  145 	strM, strS := "", ""
  146 	if result.M < 10 {
  147 		strM = "0" + strM
  148 	}
  149 	if result.S < 10 {
  150 		strS = "0" + strS
  151 	}
  152 	result.Str = fmt.Sprint(result.D) + "° " + strM + fmt.Sprint(result.M) + "' " + strS + fmt.Sprint(result.S) + "\""
  153 	if len(result.Str) > 18 {
  154 		result.Str = result.Str[0:17] + "\""
  155 	}
  156 	return result
  157 }
  158 
  159 //
  160 //
  161 //
  162 //////////////////////// Conversions longueurs/distances
  163 //
  164 //
  165 func KmToMiles(km float64) float64 {
  166 	return (km / milesCoeff)
  167 }
  168 func KmToNautics(km float64) float64 {
  169 	return (km / nauticsCoeff)
  170 }
  171 
  172 func MilesToKm(miles float64) float64 {
  173 	return (miles * milesCoeff)
  174 }
  175 func MilesToNautics(miles float64) float64 {
  176 	return (KmToNautics(MilesToKm(miles)))
  177 }
  178 
  179 func NauticsToKm(nautics float64) float64 {
  180 	return (nautics * nauticsCoeff)
  181 }
  182 func NauticsToMiles(nautics float64) float64 {
  183 	return (KmToMiles(NauticsToKm(nautics)))
  184 }
  185 
  186 //
  187 //
  188 //
  189 //////////////////////// Conversions de vitesse
  190 //
  191 //
  192 func MsecToKmh(msec float64) float64 {
  193 	return (msec * 3.6000)
  194 }
  195 func MsecToMph(msec float64) float64 {
  196 	return (msec * 3.6000 / milesCoeff)
  197 }
  198 func MsecToKt(msec float64) float64 {
  199 	return (msec * 3.6000 / nauticsCoeff)
  200 }
  201 
  202 func KmhToMsec(kmh float64) float64 {
  203 	return (kmh / 3.6000)
  204 }
  205 func KmhToMph(kmh float64) float64 {
  206 	return (kmh / milesCoeff)
  207 }
  208 func KmhToKt(kmh float64) float64 {
  209 	return (kmh / nauticsCoeff)
  210 }
  211 
  212 func MphToMsec(mph float64) float64 {
  213 	return (mph * milesCoeff / 3.6000)
  214 }
  215 func MphToKmh(mph float64) float64 {
  216 	return (mph * milesCoeff)
  217 }
  218 func MphToKt(mph float64) float64 {
  219 	return (KmhToKt(MphToKmh(mph)))
  220 }
  221 
  222 func KtToMsec(kt float64) float64 {
  223 	return (kt * nauticsCoeff / 3.6000)
  224 }
  225 func KtToKmh(kt float64) float64 {
  226 	return (kt * nauticsCoeff)
  227 }
  228 func KtToMph(kt float64) float64 {
  229 	return (KmhToMph(KtToKmh(kt)))
  230 }

 

On pourra noter ici la définition d'un nouveau type de données (ligne 22), DegDMS, DegDMS est une structure (au sens C du terme) qui permettra de stocker une donnée Degré/Minute/Seconde à la fois sous forme "numérique" (int, float64, float64) et sous la forme de chaine. En effet, selon la fonction appelante, on pourra avoir besoin de traiter cette donnée soit comme une donnée numérique, soit comme une donnée de type string.

On notera aussi la majuscule à DegDMS, ce qui rendra ce type accessible aux packages externes à ce package units.

Pour ce qui est du contenu même des différentes fonctions, rien de particulier à noter. Elles contiennent essentiellement des calculs de conversion, et/ou du formatage de chaine.

Voilà, je vous laisse expérimenter par vous même si vous souhaitez faire évoluer ce programme. Vous pouvez récupérer les 3 fichiers sources ainsi que le fichier glade ici.

Hasta luego !

Publicité
Retour à l'accueil
Partager cet article
Repost0
Pour être informé des derniers articles, inscrivez vous :
Commenter cet article