Blog traitant d'informatique, de programmation, de python, de golang, de linux...
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 !