Embedded weblog

Aller au contenu | Aller au menu | Aller à la recherche

Tag - programmation

Fil des billets - Fil des commentaires

vendredi, juin 25 2010

cours de kernel Linux en ligne

L'annonce (sur linkedIn) de ces quelques articles de début de cours sur la programmation kernel Linux me fait rappeler que des slides de formation peuvent aussi être trouvés sur le web : chez FreeElectron, et bien entendu, chez Linagora !

Bonne lecture !  :)

jeudi, février 25 2010

slides de la conférence GCC par Basile Starynkevitch

Basile Starynkevitch avait déjà donné une conférence pour Parinux il y a deux ans ; il est revenu ce 12 janvier dernier à l'espace rue de la Bourdonnais donner une conférence sur son même sujet de prédilection, le compilateur libre gcc. Sous une optique propren car il est le développeur d'un outil d'analyse de code MELT, greffon de gcc, consistant un langage de programmation pouvant être greffé à du code compilé par gcc pour effectuer de l'analyse (type tests unitaires, ça me semble très utile en tout cas dans le cadre de la certification logicielle) ; MELT est un langage complet, et le greffon MELT est codé en MELT. Ses slides sont disponibles, et je vous invite à les consulter (en réalité, deux tiers de conférence, soit 1h30, ont été consacrés à gcc en général). On constatera ainsi le franc parler de l'un des 2000 développeurs de gcc de par le monde, notamment à propos des préjugés sur le C ou l'assembleur.

jeudi, janvier 7 2010

man of the day

struct hostent *gethostbyaddr(const void *addr,
                                     socklen_t len, int type);

The gethostbyaddr() function returns a structure of type hostent for the given host address addr of length len and type type.  Valid address types are AF_INET and AF_INET6.  The host address argument is  a  pointer  to struct a of  a  type depending on the address type, for example a struct in_addr * (probably obtained via a call to inet_addr(3)) for address type AF_INET.

Les italiques sont de moi. Bon bein pour réimplémenter le bousin, je sens que ça va être fun...

mercredi, décembre 16 2009

Python et OpenOffice.org

Je me rends compte avec effroi que j'ai oublié d'écrire ce billet promis à un ami : c'est qu'il a rencontré des soucis de création de fichiers Excel dans une application Python devant faire du reporting. Problématique déjà rencontrée au sein de l'équipe embarqué, la solution a consisté en l'utilisation de PyUno, le binding Python-OpenOffice.org (et ça tombe bien, puisqu'on propose aussi des migrations sur la suite bureautique libre). La bonne nouvelle, c'est que l'on peut trouver des exemples et autres documentations très complètes (je ne vais donc pas m'amuser à tout reprendre) ; la mauvaise est qu'il faut savoir deux ou trois choses pour que ça marche dans la vie réelle (ou alors l'information importante est un peu perdue).

En premier lieu, il faut veiller à ce que votre environnement soit correct. D'une part, bien vérifier que la version de PyUno correspond à la bonne version de Python et de OOo que vous utilisez (sous Linux, les distributions assurent l'homogénéité, mais il en va autrement sous windows). Ensuite, mettre en place les bonnes variables d'environnement (évidemment, si PYTHONPATH est déjà défini, on concatène avec des ":" sous Linux, mais des ";" sous Windows qui ne fait jamais rien comme les autres -- il faut assumer son péché originel de "C:\" et les ambigüité consécutives). Sous Linux, cela se résume à :

export PYTHONPATH=:/usr/lib/ooo3/basis3.0/program/

Sous Windows en revanche, il faut définir (vous savez, en passant par le panneau de conf, "Système", onglet "Avancé", bouton "Variables d'environnement" -- mais puisqu'on vous dit que c'est simple et intuitif !) PYTHONPATH  à "C:\Program Files\OpenOffice.org 3\Basis\Program" et URE_BOOTSTRAP à "file:///C:/program%20File/OpenOffice.org%203/program/fundamental.ini" (bug documenté).

On peut à présent dans son code python faire un magnifique :

import uno

Et ça marche (rappel : on peut try-catcher des import sous Python, histoire de faire des messages d'erreur sexy au cas où l'environnement ne serait pas d'équerre, par exemple).

Arrive la subtilité de fonctionnement : PyUno s'adresse à un OOo en train de tourner via socket à travers une API, il n'utilise pas l'API de OOo directement ! Il faut donc avoir un serveur OOo qui tourne. Pour cela, il existe deux méthodes : avec ou sans l'option "-invisible" (notons aussi "-headless" qui fait la même chose mais sans avoir besoin de display -- explications par là --, et "-nologo" pour un lancement sans logo à l'affichage). Avec, nous obtenons un démon, c'est-à-dire que OOo est lancé sans fenêtre visible. Si on l'omet, une fenêtre "standard" (en apparence) s'ouvre. On pourrait penser qu'il suffit alors de lancer le serveur au démarrage de la machine, et de l'adresser ensuite. En fait c'est une erreur (attention, le passage qui suit ne convient pas aux âmes sensibles et aux enfants) : si l'on ouvre une fenêtre OOo alors que l'option "invisible" était invoqué, puis qu'on la ferme, le serveur disparaît aussi ; et de même, si l'on ferme la fenêtre-serveur (sans l'option "invisible"), le serveur aussi se ferme. C'est très, très stupide et contre-intuitif (de mémoire, j'ai eu le même problème avec "-headless"). Résultat des courses : mieux vaut lancer le serveur OOo à la volée, quand on en a besoin (au pire, si le serveur tourne déjà, c'est automatiquement détecté, aucun risque de doublon). Pour cela, rien ne vaut une fonction Python à appeler plus tard.

    def ooo_start(self):
        """ start OOo in server mode """
        if os.name == "nt":
                prog = 'start "" "C:\Program Files\OpenOffice.org 3\program\soffice.exe" -invisible -accept="socket,host=localhost,port=2002;urp;"'
        else:
                prog = 'soffice -invisible -accept="socket,host=localhost,port=2002;urp;"'
        if os.system(prog) == 0:
                self.wait_time(5)
                return True
        else:
                return False

Comme on peut le constater, cette procédure gère à la fois Windows et le reste du monde (en l'occurrence Linux, c'est bien connu). La partie optionnelle "accept=..." définit l'état de serveur par socket et l'hôte et le port. La commande Python "os.system" utilise "system" de la libC pour lancer l'application, et si cela réussit, on attend 5 secondes le temps que ça se lance (oui, gruik, je sais, mais ça marche et il n'y a pas trop le choix, puisque ça rend la main alors que ce n'est pas encore opérationnel). Il ne reste alors plus qu'à mettre en oeuvre :

        if not self.ooo_start():
                return
        local = uno.getComponentContext()
        resolver = local.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local)
        context = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
        desktop = context.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", context)

Comme on peut le constater, la succession de commandes est ésotérique, mais la bonne nouvelle est qu'il ne s'agit pas forcément de comprendre ce que l'on fait. À ce niveau, on est connecté à notre serveur PyUno, et l'idée va être maintenant de créer un document que l'on va manipuler. Par exemple, le document /home/gblanc/Documents/example.xls :

       document = desktop.loadComponentFromURL("file:///home/gblanc/Documents/example.xls" ,"_blank", 0, ())

L'idée pourrait être, pour un rapport, de copier un fichier-template à l'aide de "shutil.copyfile" et d'ouvrir la copie. Je rappelle aussi les fonctionnalités portables de manipulation de fichiers que l'on trouve dans "os.path" (qu'il s'agit d'importer auparavant) : os.path.join, os.path.basename, os.path.realpath, etc. On a vu au dessus que l'URL du document à ouvrir a été retravaillée : en effet, "file://" a été ajouté ; quiconque a déjà navigué en local avec Firefox se dit qu'il a déjà vu ce genre de syntaxe. Et que sous Windows, ça va devenir plus complexe : il va en effet falloir ajouter un "/" devant, remplacer les espaces par des "%20" (comme sous le ouèbe avant l'invention des IRL), et changer les "\" en "/" (toujours pareil, assumer les conneries des développeurs sous-doués de Microsoft des années 80). Ce qui donne au final, en prenant notre hypothèse que "fineName" contient un chemin standard de fichier (tel que retourné par une boîte de dialogue Qt, au hasard) :

        if os.name == "nt":
                fileName = "/" + os.path.realpath(fileName).replace(" ", "%20").replace('\\', '/')
        else:
                fileName = os.path.realpath(fileName)
        document = desktop.loadComponentFromURL("file://" + fileName, "_blank", 0, ())

Notre variable "document" est donc à présent notre document de travail sur lequel on va pouvoir agir (remarque : notre fileName se terminant par ".xls", ".ods" ou assimilé, c'est Calc qui est lancé automatiquement par OOo). À ce niveau-là, je ne saurais que trop recommander d'entrer les commandes précédentes sur un iPython, et d'utiliser la tabulation pour constater à quel point l'API est aussi riche que labyrinthique, et qu'un outil d'autocomplétion et de documentation (souvent absente) peut nous être utile. L'API est calquée en réalité (m'a-t-on affirmé) sur celle des macros. D'un autre côté, ai-je une tête à faire des macros sous OOo ?? (en revanche, nous avons ça à Linagora, mais ce n'est pas moi qui m'en occuperait, n'est-ce pas ?). Bref, quelques fonctions utiles :

        document.Sheets.getByIndex(0).getCellByPosition(0, 0).setString("du texte")
        document.Sheets.getByIndex(0).getCellByPosition(0, 1).setValue(42)

Trois remarques : d'abord dans une feuille Excel (ou ods), se déplacer plutôt par "Sheets.getByIndex" plutôt qu'en appelant le raccourci du nom de l'onglet, histoire de pouvoir s'en sortir le jour où un gus renommera l'onglet du modèle. Ensuite, "getCellByPosition" se déplace selon le même système de coordonnées que sur une matrice. Enfin, il faut différencier "setString" qui ajoute du texte (qui peut être un chiffre) de "setValue" qui ajoute un nombre à la cellule : pour faire des calculs, par exemple, c'est "setValue" qu'il faut utiliser, sinon le résultat est juste catastrophique (et lorsqu'on n'est pas au courant, pour débuguer, on reste un peu bête).

Comme on peut s'en apercevoir lorsqu'on entre beaucoup de valeurs (plus d'une centaine, disons), c'est franchement super-lent comme traitements. Rassurez-vous : toute usine à gaz aura les mêmes problèmes, inutile de pester. On peut rendre en revanche la chose plus intéractive pour l'utilisateur, qui devant son écran et au lieu de se tourner les pouces (ou jouer au démineur) pourra voir les jolies valeurs être entrées auto-magiquement, ce qui est toujours ulta-sex et donne l'impression au client d'avoir payer une appli qui poutre. Pour cela, il existe dans notre document une sous-classe "controller" qui va permettre d'en prendre le contrôle (on l'aura deviné) :

        controller = document.getCurrentController()
        controller.setActiveSheet(document.Sheets.getByIndex(1))

Et paf que ça change l'onglet de la feuille Excel (ou Ods, etc, bref Calc) dans notre OOo Calc ! C'est juste beau.

Il s'agit à présent que toutes les données ont été entrées de sauver le document (sinon, il est perdu, ce serait bête) :

        document.store()

On peut aussi faire un :

       document.dispose()

Mais cela ferme la fenêtre et le serveur, ce qui peut être regrettable lorsqu'il s'agit de vérifier qu'un rapport a été bien rempli comme il convient (cf notre problématique initiale).

Voilà c'est tout (ouf !). Aucun plantage n'a jamais été signalé ni constaté, et aucune mouette n'a été blessé (ni mordu par un serpent). Vous avez économisé une licence de l'affreux concurrent Microsoft Office (qui dispose d'un binding Python crassouillet sous windows ; et d'une méthode par ActiveX sinon, il me semble, mais c'est gore, n'est-ce pas ?), vous avez un code portable, sans passer par cette horreur de Perl et son API Excel (qui a le mérite d'exister, mais le problème intrinsèque de Perl, c'est que ça existe tout court), et grâce à ce tutoriel, c'est d'une simplicité relative (sinon, contactez notre zélé commercial Cédric Ravalec, il se fera une joie de vous proposer de l'AT dans la demi-heure). C'est beau.

mercredi, décembre 2 2009

le code qui fait mal aux yeux du jour

J'en reste sur le postérieur : il n'y a pas de fonction sleep en Javascript (gare à celui qui me demande ce que je fiche avec un truc pareil !). En cherchant sur le net, on trouve des solutions de callback scabreuse, et une vieille méthode, qui comment dire, bref...

function sleep(time){
     var start = date.getTime();
     while(start+time > date.getTime()) true;
     return;
}

J'espère que vous ne venez pas de manger. Ça ferait presque penser aux vieux temps du nop en asm pour occuper le CPU. On est en 2009, un langage de programmation ne propose pas de moyen de faire une pause autre que d'occuper 100% des ressources du CPU en attente active ; après demande aux webeux linagoriens, incrédule, confirmation a été donnée. Tout à coup, une citation de Linus Torvalds m'est revenue :

Modern PCs are horrible. ACPI is a complete design disaster in every way. But we're kind of stuck with it. If any Intel people are listening to this and you had anything to do with ACPI, shoot yourself now, before you reproduce.

J'en pense de même pour les créateurs du Javascript...

jeudi, septembre 17 2009

le code de la soirée

_colorline = ['#%02x%02x%02x' % (25+((r+10)%11)*23,5+((g+1)%11)*20,25+((b+4)%11)*23) for r in range(11) for g in range(11) for b in range(11) ]

(c'est dans OpenERP)

(et c'est du Python)

mardi, septembre 1 2009

le serpent et le troll

Comme je le dis toujours à mes étudiants, dans ce métier, de nos jours, il faut savoir tout faire, depuis l'assembleur (mécanique les mains dans le cambouis) jusqu'à l'Ajax (et la cuisine brille !). Voilà comment un ingénieur en informatique embarquée se retrouve à faire de l'IHM pour son backend (qui, je vous rassure chers lecteurs, est de type pilotage de matériel, il ne faut pas pousser non plus).

Une Interface Homme Machine, GUI en anglais, le nom fait peur ; de nos jours, on a plutôt tendance à faire dans le web service, à force de planter des projets de "progiciels" (déjà, quand on s'amuser à changer les noms, c'est qu'il y a du louche). Mais ce qui est demandé par le client, c'est du clickodrôme simple, efficace, qui propose d'exécuter différentes actions. Mon idée : Python + QT. Le résultat : impressionnant. Et en plus (surtout, pour le projet qui m'intéressait) : extrêmement portable (Linux/Windows/Mac/BSD/p'têtre même Solaris, c'est dire).

Prérequis : savoir coder en Python (ce que je prendrai réellement comme prérequis dans cet article : c'est assez facile à appréhender), et connaître la bibliothèque (notamment graphique, mais pas que) Qt de Trolltech, c'est-à-dire à présent Nokia (ce que l'on va étudier ici). Je vous rassure, avant de commencer ce projet, je ne connaissais ni l'un ni l'autre, mais j'en avais un bon a priori. Avec une autoformation rapide (d'abord en Python) et du développement itératif (on dit "méthode agile" pour faire style, de nos jours, mais je préfère toujours les histoires de bazar contre les cathédrales, même en développant seul -- avec mes multiples personnalités, certes, mais elles ne sont pas rémunérées --, ce qui ma foi réussi toujours -- on nomme ceci la "méthode à Gilles"), on y arrive très bien (ce que l'on dit toujours après avoir galéré deux heures à chercher une feature toute bête dans son esprit, et qui finalement marche du feu de dieu) ; merci le Qt Assistant (l'aide complète que l'on peut trouver dans le Qt Designer ou sur le net) et le Python reference manual ; et les (trop) nombreux sites web d'(entre-)aide (bonne chance pour trouver, cependant).

Commencez par installer Python, Qt, et le binding PyQt qui va bien avec vos versions Python/Qt (toute bonne distrib' Linux possède des paquets homogènes, sous windows ce n'est pas bien complexe, pour les autres débrouillez-vous vous l'avez cherché). Juste une précision sur les licences : Python est interpréteur de langage script, il n'y a donc aucun impact ; Qt est passé en LGPL sur ses dernières versions (>=4.5), cela n'a donc pas d'incidence sur votre code (seulement sur les éventuelles modifications que vous apportez à la bibliothèque même) ; en revanche, le binding est sous GPL, ce qui implique donc que votre code le sera aussi. La société privée qui développe le code (et il faut bien qu'ils mangent) propose en revanche une licence commerciale ; le business model est donc similaire à celui de Trolltech (peu) avant son rachat par Nokia (qui se moque à présent de faire de l'argent avec : ils voulaient surtout récupérer leurs technos pour faire... de l'embarqué bien sûr !).

On commence simplement : ouvrir le Qt Designer (Qt Creator maintenant, on n'arrête pas le progrès), et demander de créer une nouvelle fenêtre principale. Hop, la voilà qui s'affiche, on peut à présent la décorer de multiples boutons, label, menus, groupes, et j'en passe, le tout par simple glisser déplacer ; une fenêtre permet aussi de changer les propriétés des éléments graphiques, y compris la fenêtre elle-même (l'aide peut se trouver dans l'assistant). On enregistre : ça crée un fichier en ".ui" (user interface devine-t-on), qui n'est autre que du XML. Il va falloir passer ce fichier à la moulinette pour produire du code non pas C++ mais Python : pour cela, pyuic4 est à disposition (on suppose le fichier en ".ui" enregistré dans le répertoire "ui/").

pyuic4 -o ui_projwindow.py ui/projwindow.ui

Le fichier généré comporte alors une classe du nom de la fenêtre que vous avez créé (propriété Name renseignée dans le designer), par exemple "Ui_ProjWindow" ; celle-ci comporte deux fonctions, "setupUi" qui crée la fenêtre (chaque élément, bouton, label, etc, est créé et ajouté à la fenêtre, et ses propriétés initialisées), et "retranslateUi", qui appelée par la première fonction met en place les textes dans la langue que vous avez choisi. D'ailleurs, ce qui est chouette, avec Qt, c'est la localisation, et c'est ainsi que vous n'avez pas à vous soucier de remplacer "yes" par "oui" dans les boîtes de dialogue : il suffit simplement de connaître les bonnes incantations magiques (voir plus bas).

Nous avons donc une classe fenêtre, il va falloir l'appeler pour qu'elle s'affiche. La tradition agit en deux temps : d'une part une petite instanciation générale dans un fichier ".pyw" (l'extension est mal reconnu sous Linux, mais très bien sous windows ; donnez lui le droit de s'exécuter sous Linux, et appelez-le directement), et d'autre part le code massif de l'application en elle-même. Soit "proj.pyw" la première, et projwindow.py la seconde.

Disséquons proj.pyw :

#!/usr/bin/env python
# -*- coding: iso8859-1 -*-

# on importe de quoi créer une application graphique
from PyQt4.QtGui import QApplication
# on importe un minimum de la bilbiotheque (ce qui est dans QtCore n'est pas graphique)
from PyQt4.QtCore import Qt, QLocale, QTranslator, QLibraryInfo, QString
# on importe la classe qui va gerer l'application graphique
from projwindow import ProjWindow

# on entre ici des l'execution de ce fichier
if __name__ == "__main__":

# creation de l'application
    app = QApplication(sys.argv)

# incantations pour traduire la fenetre Qt dans la bonne langue
    locale = QLocale.system().name()
    translator = QTranslator ()
    translator.load(QString("qt_") + locale,
            QLibraryInfo.location(QLibraryInfo.TranslationsPath))
    app.installTranslator(translator)

# creation de l'interface graphique complete
    window = ProjWindow()
# affichage d'icelle (bloquant)
    window.show()
# en attente de fermeture
    sys.exit(app.exec_())

Jusqu'ici, rien de sorcier. On peut s'amuser à créer un hook (un crochet, quoi) qui affiche les exceptions Python dans une boîte de dialogue simple. Pour cela, après la création de l'application, insérer la ligne "sys.excepthook = excepthook", et définir au dessus de notre code la fonction "excepthook" :

import sys, os, traceback, time
from PyQt4.QtGui import QDialog, QLabel, QVBoxLayout

def excepthook(except_type, except_val, tbck):
    """ crochet python pour gerer les exceptions """
# on recupere la pile d'execution
    tb = traceback.format_exception(except_type, except_val, tbck)
# on cree une boite de dialogue avec un bouton de fermeture
    diag = QDialog(None, Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint)
# defintion du titre
    diag.window().setWindowTitle("Une exception est survenue")
# la trace est une liste, on la concatene avec "join" et on la met dans un label
    lab = QLabel(''.join(tb))
# le label gere les retours a la ligne automatiques (evite d'avoir une boite qui fait la largeur de l'ecran)
    lab.setWordWrap(True)
# on donne la possibilite de selectionner le texte affiche avec la souris
    lab.setTextInteractionFlags(Qt.TextSelectableByMouse)
# on cree une boite verticale
    layout = QVBoxLayout()
# on ajoute le widget label cree au dessus au layout
    layout.addWidget(lab)
# on defini le layout par le precedent
    diag.setLayout(layout)
# on affiche la boite de dialogue
    diag.exec_()

On voit déjà avec cet exemple comment on crée "manuellement" une boîte de dialogue simple (sans bouton, juste un label) et comment on l'affiche. Plusieurs concepts apparaissent déjà : codé en C++, Qt est très hiérarchisé, et tout élément graphique hérite de la classe "QWidget". Les widget sont organisés selon des "layout", soit horizontaux soit verticaux, qui peuvent s'imbriquer. Voir à ce propos cette page très complète (en C++ forcément, mais vous êtes au moins bilingue). On remarque que le "exec()" de C++ est devenu "exec_()" en Python, tout simplement pour éviter une fâcheuse ambigüité, il faut ainsi prendre garde à ce genre d'exceptions très rares au niveau du binding. D'ailleurs il faut bien se rendre compte du travail de lien entre C++ et Python, qui peut ne pas être exempt de bugs (une mauvaise utilisation de la bibliothèque, au lieu de remonter une exception, peut très bien faire segfaulter l'application -- cela reste assez rare). Enfin, il faut remercier ce projet de gestion complète de graphes (un vrai bonheur d'inspiration) pour m'avoir donné l'idée de la fonction ci-dessus (qui s'avèrera je suis sûr fort pratique en cas de bug chez le client -- sait-on jamais, ma divinité est grecque et limitée -- pour nous remonter l'information) : je vous invite à aller admirer ce que l'on peut faire dans le genre hyper-poussé en PyQt !

Mais revenons-en à la création de notre interface en elle-même. Dans le fichier projwindow.py, placé dans le même répertoire, on va trouver la définition de la classe ProjWindow, qui hérite l'interface graphique "visible" créée dans le designer, et à laquelle va être "hooké" des fonctions diverses et variées.

# -*- coding: iso8859-1 -*-

import os, sys
from PyQt4.QtCore import pyqtSignature, QString, Qt, QVariant, QRect, QRectF, QThread, QEvent, QSize, SIGNAL, SLOT
from PyQt4.QtGui import *

from ui_projwindow import Ui_ProjWindow

class ProjWindow(QMainWindow, Ui_ProjWindow):
    def __init__(self, parent = None):
    QMainWindow.__init__(self, parent)
    self.setupUi(self)

Comme on voit, l'initialisation de la classe "ProjWindow", qui hérite de "Ui_ProjWindow" va appeler "setupUi", initialisant les graphismes. Mais cela après avoir appelé l'initialisation de QMainWindow, en faisant dès lors la fenêtre principale du projet (car on peut aussi créer les boîtes de dialogue ou les fenêtres filles avec le designer). On remarque au passage que l'application est codée en iso8859-1, ce qui permet de mettre des accents (faire attention lors de l'enregistrement du fichier) ; je ne l'ai pas mis en utf8 pour cause de problèmes avec l'affichage de texte dans widget purement graphiques (j'y reviendrai), mais on note que le générateur Qt-Python crée les fichiers en utf8, permettant donc l'inclusion de tous les caractères de la création. Il faut à présent compléter la fonction d'init de tout ce que l'on souhaite pour l'initialisation de notre application graphique (création de variables locales à l'objet, calculs divers, ouverture de fichiers, etc), et surtout : des crochets pour l'interactivité.

En Qt, l'intéractivité se gère via les signaux. On peut trouver de la bonne littérature sur le sujet, mais il faut déjà veiller à réadapter la syntaxe pour Python. Prenons quelques exemples.

    self.connect(self.menuSave, SIGNAL("triggered()"), self.menu_save)

On connecte ainsi le "signal" nommé "triggered()" de "menuSave" (un item de menu) à la fonction "menu_save(self)" (qui reste à définir plus bas dans la classe). Je rappelle que "self." indique que l'on se réfère à des procédures ou des variables de l'objet lui-même (équivalent de "this" en C++ ou Java, à ceci près qu'en Python c'est obligatoire de préciser). "triggered()" correspond à un clic sur un menu. On peut trouver "clicked()" sur un bouton, "toggled(bool)" pour une boîte de sélection à cocher (indique que l'on vient de cliquer sur l'un des éléments de la boîte), tout comme il y a "valueChanged(int)" sur un slider ou une boîte numérique (la fonction hookée devra alors prendre en argument un entier, qui prendra la valeur de l'objet graphique), ou encore "currentIndexChanged(QString)" pour une boîte de sélection, à différencier de "currentIndexChanged(int)" (le premier renvoie la chaîne de caractère de ce qui a été sélectionné, le second le numéro de l'index ; le traitement est donc différent dans la fonction appelée, sachant cependant que l'on peut ensuite récupérer ses informations en questionnant les propriétés de l'objet).

On peut aussi s'amuser à "automatiser" le traitement de signal, par exemple le fait de changer la combo box "combo_texts" change le texte du label "label_textcombo".

    self.label_textcombo = QLabel(self)
    self.combo_texts = QComboBox(self)
    self.combo_texts.addItem("toto")
    self.combo_texts.addItem("tata")
    self.combo_texts.addItem("titi")
    self.connect(self.combo_texts, SIGNAL("valueChanged(QString)"), self.label_textcombo.setText)

Pour ne pas se casser la tête, et parce que coder fait mal aux doigts (après il faut traiter l'arthrite, ça fait cher pour la SECU), Qt Designer dispose d'une petite fenêtre sympathique "Signal/Slot Editor" (il y a plusieurs autres onglets : "Action Editor" qui permet de gérer les menus, notamment d'ajouter des raccourcis clavier ; et "resource browser", qui doit servir à gérer les fichiers de langue, par exemple, à vue de nez), qui permet de choisir un élément graphique d'émission, un signal (comme on a vu), un autre élément graphique cette fois de réception, et un slot associé (c'est-à-dire une fonction interne comme "setValue(int)", "setText(QString)", etc) ; tout sera généré ensuite automatiquement en Python.

Évidemment, à toute règle ses exceptions : gérer le surlignement à la souris (passage de la souris par dessus un widget, par exemple un label) se fait via une surcharge de fonction :

        self.my_label.enterEvent = self.my_labelEnter

    def my_labelEnter(self, event):

De même lorsque la souris quitte notre label, ou lorsqu'on clique dessus :
        self.my_label.mousePressEvent = self.my_labelSelect
        self.my_label.leaveEvent = self.my_labelLeave

Idem pour la roulette de la souris, par dessus un QGraphicsView (cf plus bas) pour zoomer, par exemple :

       self.mygraph.wheelEvent = self.wheelZoomGraph

    def wheelZoomGraph(self, event):

Pour les événements de redimensionnement de la fenêtre et de fermeture, il suffit donc de surcharger les fonctions :

    def closeEvent(self, event):

    def resizeEvent(self, event):

Noter que l'on peut annuler un événement :

        event.ignore()

Vous en savez à présent assez pour écrire les doigts dans le nez (pardon, sur le clavier) un classique "hello world", et donc recoder avec un peu plus de temps l'ensemble des applications KDE. Il suffit juste de connaître quelques autres trucs et astuces (liste évidemment non exhaustive).

Création d'une boîte de dialogue simple :

    QMessageBox.critical(self, "Erreur !", "Une action a tout cassé.")

La boîte s'affiche toute seule, et attend que l'on clique sur "OK". D'autres boîtes prédéfinies existent, parfois avec plusieurs boutons :

    ret = QMessageBox.question(self, "Question existentielle", "Suis-je buggué ?",
                       QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
    if ret == QMessageBox.No:
        return False

Pour ajouter des boutons personnalisés à une boîte de dialogue standard (on peut aussi ajouter des boutons prédéfinis tel QMessageBox.Abort) :

    msgBox = QMessageBox(QMessageBox.Critical, "Erreur !", "Une action a tout cassé.", QMessageBox.Ok, self)
    quitButton = msgBox.addButton("Quitter urgemment", QMessageBox.ActionRole)
    ret = msgBox.exec_()
    if msgBox.clickedButton() == quitButton:
        sys.exit()

Pour ajouter une option à notre boîte (on s'arrêtera là, on peut tout personnaliser, ça risque de faire long) :

    ch = QCheckBox("Redemander inlassablement", None)
    ch.setCheckState(Qt.Checked)
    msgbox = QMessageBox(QMessageBox.Question, "un tas d'étapes", "Voulez-vous passez à l'étape #2 ?",
                   QMessageBox.Yes | QMessageBox.No, self)
# notez ici l'utilisation du layout pour ajouter le widget
    msgbox.layout().addWidget(ch, 1, 1)
    ret = msgbox.exec_()
    if ret == QMessageBox.No:
        return
    # faire des trucs
    if ch.checkState() == Qt.Checked:
        msgbox.setText("Voulez-vous passer à l'étape #3 ?")
        ret = msgbox.exec_()
        if ret == QMessageBox.No:
            return
    #etc

Il existe aussi des boîtes de dialogue prédéfinies pour diverses actions (que l'on devinera aisément) :

    dirname = QFileDialog.getExistingDirectory(self, "Sélectionner un répertoire", default_path)

    file = QFileDialog.getOpenFileName(self, "Ouvrir un fichier", default_path, "Text (*.txt *.odt)")

    file = QFileDialog.getSaveFileName(self, "Enregistrer", "monbidule", "*.txt")
    if not file:
        return
    if not file.endsWith(".txt"):
        file += ".txt"

Autre fonctionalité sympathique et un peu cachée, l'affichage de texte dans la bande du bas de la fenêtre principale ("status bar") ; ici, durant 4 secondes (optionnel, mais penser alors à effacer manuellement en définissant un texte valant "") :

    self.statusBar().showMessage("la vie est belle", 4000)

Pour afficher/masquer et rendre disponible/indisponible un objet graphique, on procède avec :

   # griser
    self.mything1.setEnabled(False)
    # cacher
    self.mything2.setVisible(False)

Pour changer un texte de couleur et de propriété (c'est un peu lourd, mais extrêmement flexible ensuite) :

    self.color_text_black = QPalette()
    self.color_text_black.setColor(QPalette.WindowText, QColor(0, 0, 0))
    self.color_text_green = QPalette()
    self.color_text_green.setColor(QPalette.WindowText, QColor(65, 155, 50))
    self.color_text_red = QPalette()
    self.color_text_red.setColor(QPalette.WindowText, QColor(185, 50, 20))
    self.fontItBold = QFont()
    self.fontItBold.setItalic(True)
    self.fontItBold.setBold(True)
    self.fontDefault = QFont()
    # vert !
    label.setPalette(self.color_text_green)
    # italique
    label.setFont(self.fontItBold)
    # retour au noir
    label.setPalette(self.color_text_black)
    # font normale
    label.setFont(self.fontDefault)

Il reste l'épineuse question des threads à l'intérieur de l'application, notamment pour les traitements lourds : il est toujours pénible de voir la fenêtre se geler durant un temps indéfini. La méthode peut consister en l'affichage d'une boîte de progression, les exemples sur internet ne manquent pas. Pour cela, Qt met à disposition QThread. Voici comment procéder :

    self.mythread = QThread()
    self.mythread.run = self.myaction
    self.mythread.start()

La fonction myaction sera alors appelée dans un thread. Attention : "mythread" doit bien être une variable de l'objet (ici "WindowProj"), et donc initialisée à "None" avant "__init__", sous peine de voir le thread victime du garbage collector dès la sortie de la fonction appelante ! Une fois dans myaction, on peut faire ce que l'on veut (enregistrer un gros fichier par exemple), sauf interagir directement avec les graphismes. C'est-à-dire qu'il n'est pas possible d'afficher directement une boîte de dialogue "enregistrement terminé", par exemple (ou de le mettre dans la status bar, histoire d'être plus poli). Pour ce faire, il faut passer par des événements personnalisés.

    def myaction(self):
# creation d'un evenement personnalise
        event = QEvent(QEvent.User)
        event.event_type = "my_event"
        # faire des choses interessantes
        if success == False:
# on a detecte que ca a foire
            event.event_action = "failure"
        else:
# on est trop doue (penser a demander une augmentation)
            event.event_action = "success"
# on "poste" notre evenement
        qApp.postEvent(self, event)

# cette fonction est une surcharge pour traiter des event persos
    def customEvent(self, event):
# on filtre
        if event.event_type == "my_event"
            if event.event_action == "success":
                self.statusBar().showMessage("enregistrement réussi", 5000)
            elif event.event_action == "failure":
                QMessageBox.warning(self, "Erreur !", "enregistrement echoue, retenter ?", QMessageBox.Yes | QMessageBox.Abort)


Notons qu'avec Python, on peut ajouter n'importe quel champ simplement à customEvent, sans avoir à le faire hériter (attention à ne pas faire ensuite référence à une variable non existente dans le traitement !). Mais disons que dans l'ensemble, c'est un peu lourd, et que l'on a parfois des résultats pas très chrétiens. Personnellement, je préfère, lorsque l'action peut être découpé en petits morceaux (par exemple pour un enregistrement de fichier dans OpenOffice.org, avec le binding PyUno, on a une ou plusieurs boucles), placer à intervale régulier un :

    QApplication.processEvents()

Ce qui va forcer le traitement des événements en attente sur la fenêtre, et notamment la redessiner. On peut ainsi continuer de cliquer, et tout s'exécutera dans les threads "automatiques" de traitement habituels ; attention cependant à ne pas se prendre les pieds dans le tapis (par exemple, modifier les données que l'on est en train d'enregistrer, ou relancer cette commander : bref, il faut penser à verrouiller ce qui doit l'être, sinon gare au crash !).

Python étant un langage objet, on dispose des mêmes possibilités qu'en C++, et notamment on peut redéfinir certaines fonctions. Citons "resizeEvent(self, event)" et "closeEvent(self, event)" qui si elles sont redéfinies dans notre "ProjWindow" vont permettre d'intercepter les événements de redimensionnement et de fermeture de la fenêtre principale (et donc de l'application pour la fermeture), et pourquoi pas de les annuler avec "event.ignore()".

Gérons un peu de graphisme. Le widget qui sert à afficher est QGraphicsView :

    self.mygraph = QGraphicsView(self)

Il s'agit alors de lui ajouter une "scène", dans laquelle on peut dessiner. On peut ainsi définir plusieurs scènes, et les dessiner hors écran, et les interchanger à l'affichage.

    self.scene_img = QGraphicsScene(self)
    img = QPixmap("graphics/img.jpg")
    self.scene_img.clear()
    self.scene_img.addItem(QGraphicsPixmapItem(img))
    self.mygraph.setScene(self.scene_img)

On peut ainsi ouvrir quasiment tous les formats de fichiers d'image de la création, on peut coller du texte formaté en html, et j'en passe (par exemple, on peut récupérer l'image d'un widget non affiché grâce à "QPixmap.grabWidget(my_widget)", ou encore faire une capture d'écran). Le problème principal est de ne pas s'emmêler entre les QPixmap, les QImage, les QGraphicsPixmapItem -- et ce n'est vraiment pas une mince affaire. Une fois que l'on commence à devenir un Jedi, on peut commencer à ajouter du texte, à zoomer avec la roulette, puis à zoomer automatiquement pour ajuster la vue, et jouer avec la matrice pour ajouter des effets de rotation, etc (il y a des fonctions toutes faites, mais pour savoir comment les utiliser, ça doublerait presque la taille de ce billet -- et puis, je ne vais pas non plus tout vous dire, comment on fait tourner la boîte ensuite, hein, je vous le demande ?).

Il ne reste plus qu'à imprimer !

    def export_to_printer(self):
# creation de l'imprimante
        printer = QPrinter(QPrinter.HighResolution)
# on est en A4 et en couleur
        printer.setPageSize(QPrinter.A4)
        printer.setColorMode(QPrinter.Color)
# différents champs
        printer.setCreator("Dieu")
        printer.setDocName("ma belle image")
# gestion de l'export en PDF (chouette non ?)
        printer.setOutputFileName("image.pdf")

# affichage de la boite de dialogue (automatique)
        dialog = QPrintDialog(printer, self)
        ret = dialog.exec_()
        if ret != QDialog.Accepted:
            return

# creation d'un widget de dessin
        painter = QPainter()
# optionnel : l'antialiasing
#        painter.setRenderHint(QPainter.Antialiasing, False)
#        painter.setRenderHint(QPainter.TextAntialiasing, False)
        self.export_to_pages(painter, printer)

    def export_to_pages(self, painter, printer):
        """ export_to_pages: export the scene on painter with a number of pages
        """
# calcul du nombre de page : arrondi superieur de la hauteur de la scene sur celle du papier A4
        w = printer.pageRect().width()
        h = printer.pageRect().height()
        numPage = int(round(self.scene_img.height() / h))
# on initialise la zone dans laquelle tout ce qui passera par "render" sera sorti sur le QPrinter
        painter.begin(printer)
# boucle d'impression
        for page in range(0, numPage):
# capture de la partie A4 de la scene qui va bien, pour l'envoyer sur papier (avec la meme taille)
            self.scene_img.render(painter, QRectF(0, 0, w, h), QRectF(0, h * page, w, h))
# si l'on cherche a rajouter du texte supplementaire, comme un titre
#           painter.drawText(20, 16, "toto")
# demande de nouvelle page
           if page < numPage - 1:
               printer.newPage()
# fin du rendering
        painter.end()

Voilà c'est tout. On remarque que j'ai sous-traité le "rendering" dans une sous-fonction "export_to_pages" : il s'agit en fait de factoriser l'export en PDF. En effet, en Qt, on peut générer aussi facilement du PDF que ce que l'on imprime. Pour ce faire :

    def export_to_pdf(self):
# boite de dialogue habituelle
        file = QFileDialog.getSaveFileName(self, "Exporter en PDF", "mon_pdf", "*.pdf")
# le printer et le painter
        pdfPrinter = QPrinter()
        pdfPainter = QPainter()
# on veut sortir en PDF !
        pdfPrinter.setOutputFormat(QPrinter.PdfFormat)
# si l'on veut changer la taille du papier manuellement
#        pdfPrinter.setPaperSize(QSize(420, 420), QPrinter.Point)
# mais nous on veut du A4
        pdfPrinter.setPaperSize(QPrinter.A4)
# pour rogner les marges (pas très joli, et l'imprimante risque de vous en vouloir)
#        pdfPrinter.setFullPage(True)
# diverses proprietes (consultable dans votre lecteur PDF favori)
        pdfPrinter.setCreator("Dieu")
        pdfPrinter.setDocName("mon joli PDF")
# definition du nom de fichier de sortie
        if not file.endsWith(".pdf"):
        file += ".pdf"
        pdfPrinter.setOutputFileName(file)
# et l'export tout pareil qu'avant
        self.export_to_pages(pdfPainter, pdfPrinter)

Simple et efficace -- quand on sait comment manier la bête, mais maintenant, vous savez. D'ailleurs, il ne me reste plus grand chose d'important à vous apprendre, chers lecteurs. Allez, peut-être, en bonus : faire un logo de démarrage.

# le texte (a readapter, merci)
splashcopyr='''<font color="black"><b>My Appli v0.2beta<br></b>
Copyright (C) Linagora<br>
Conçu par LINAGORA (Gilles Blanc, gblanc@linagora.com)</font>
'''

def makeSplashLogo():
    """Make a splash screen logo."""
    border = 16
# la taille de l'image a l'ecran, attention au ratio
    xw, yw = 473, 427

# ouverture de "./graphics/logo.png"
    pix = QPixmap(os.path.realpath(os.path.dirname(__file__)) + "/" + 'graphics/logo.png')
# on dessine l'image
    p = QPainter(pix)

# creation d'un document texte
    doc = QTextDocument()
    doc.setPageSize(QSizeF(pix.width(), pix.height()))
    f = qApp.font()
# on ecrit en petit
    f.setPointSize(8)
    doc.setDefaultFont(f)
# on ecrit centre
    doc.setDefaultTextOption(QTextOption(Qt.AlignCenter))
# et paf, on interprete du html en deux coups de cuillere a pot !
    doc.setHtml(splashcopyr)
# translation en bas de l'ecran du texte
    p.translate(0, pix.height() - 80)
# on dessine le texte
    doc.drawContents(p)
# fin du dessin
    p.end()
    return pix

if __name__ == "__main__":

    app = QApplication(sys.argv)
    sys.excepthook = excepthook

# creation du splash screen
    splash = QSplashScreen(makeSplashLogo())
# on affiche le splash screen
    splash.show()
    app.processEvents()

    from tdsdgwindow import TdsDgWindow
#etc (ou retour a la case depart)

Ceci dit, c'est vraiment du cosmétique : les applications Python-Qt s'affichent très vite ! Grosso modo, avec un total de plus de 3000 lignes Python, dont près de 900 générés pour la création du graphisme de la fenêtre, mon application (enfin, la plus grosse) met moins de trois secondes pour s'afficher ! (et bouffer au passage 35Mo de mémoire, environ) En revanche, pour une exécution qui nécessite une recompilation en bytecode du Python (création des fichiers ".pyc" associés aux différents ".py"), il faut deux fois plus de temps. Ce qui normalement, lorsque l'on fait une livraison, n'arrive qu'une seule fois tout au plus.

J'allais oublier un problème épineux (j'avais bien dit pourtant au début que j'y reviendrai, souvenez-vous...) : l'encoding -- on a un nom en French pour ça ? Heu... --, l'encodage de caractères. De nos temps modernes, l'usage de l'utf8 est devenu très courant (tout ça parce qu'il y a des types sur Terre qui ne parlent pas anglais, j'vous jure... Bref). Or, on l'a vu, le fichier projwindow.py est en ascii tout simplement parce que sinon, il se passe des drames dans l'affichage de texte sur les QGraphicsView (vous savez les caractères étranges). Du coup, pour insérer une date en français avec accent (à tester en août, ni en juillet ni en septembre), il faut :

painter.drawText(42, 42, time.strftime("%A %d %B %Y, %H:%M").decode('utf-8')

Pour insérer à présent un texte dans une scène avec "addText", en allant le récupérer depuis un texte de QLabel, on transforme la QString en objet Python "unicode" (ie une "str" en utf-8) :

    caption = unicode(my_label.text())
    text = self.scene_img.addText(caption)
    text.setPos(42, 42)

Voilà, je crois avoir fait le tour de ce qui est le plus important à connaître (je vous ai passé "comment faire un zoom sur une zone graphique avec la roulette", ne m'en veuillez pas, il faut que je garde un peu de savoir-faire sinon je risque le chômage).

Une fois que vous serez passé maître jedi, vous pourrez alors recoder elasticnodes.py, du répertoire "examples/graphicsview" dans les sources du binding, totalement impressionnant. Mais il faudra peut-être lutter un peu, et parcourir un long chemin (petit scarabée), alors sait-on jamais que vous ayez un DIF de côté, contactez-moi donc, je suis sûr que l'on pourra s'arranger...  ;)



Best of webographique :
* sur le site de Python, les bindings de GUI et PyQt en particulier
* des slides de présentation
* intro par la création d'une appli
* intro par l'exemple
* un tuto très complet et très bien fait

mercredi, mai 27 2009

au détour d'une macro crasseuse

#define ATOI2(ar)       ((ar) += 2, ((ar)[-2] - '0') * 10 + ((ar)[-1] - '0'))

Ça se trouve au milieu de date.c, de l'utilitaire date d'OpenBSD. Ça transforme tout nombre entre 10 et 99 (ou 00 et 99) en chaîne de caractère en integer (ou au moins en char), et ça passe même sur un gcc très récent. Outre l'idée d'avancer de deux dans le pointeur puis de référencer en négatif, et de flinguer au passage la variable d'entrée, autre chose interpelle : la syntaxe.

int i = 42;
int t = (2, i++, 4 + i);
printf("%d ", t);

Ceci affiche 47 !! Page 210 de votre K&R (A7.18), opérateur "," :

f(a, (t=3, t+2), c)
a trois arguements et le deuxième d'entre eux vaut 5.

Toutes les personnes interrogées, développeurs expérimentés en C, ne connaissaient pas ; je me suis senti moins seul...

jeudi, décembre 4 2008

comment inline changea le monde

Enfin, je pense. Inline, ce n'est pas dans le C, à la base, ai-je appris ce matin ; manifestement, ça n'a été standardisé qu'en 99, alors que GCC le gérait déjà depuis belle lurette (j'ai déjà dit à quel point gcc était génial ? Ah, oui...). Alors comment faisait-on avant ?

Eh bien manifestement, c'est horrible, mais on n'utilisait que les macros. Retenez votre respiration, cher lecteur (de plus de 16 ans, en dessous, fermez votre navigateur immédiatement !), et allez voir par ici...

jeudi, novembre 6 2008

à quoi sert typedef ?

A éviter ça ?

snmp_sess_add_ex(netsnmp_session * in_session,
                 netsnmp_transport *transport,
                 int (*fpre_parse) (netsnmp_session *, netsnmp_transport *,
                                    void *, int),
                 int (*fparse) (netsnmp_session *, netsnmp_pdu *, u_char *,
                                size_t),
                 int (*fpost_parse) (netsnmp_session *, netsnmp_pdu *,
                                     int),
                 int (*fbuild) (netsnmp_session *, netsnmp_pdu *, u_char *,
                                size_t *),
                 int (*frbuild) (netsnmp_session *, netsnmp_pdu *,
                                 u_char **, size_t *, size_t *),
                 int (*fcheck) (u_char *, size_t),
                 netsnmp_pdu *(*fcreate_pdu) (netsnmp_transport *, void *,
                                              size_t))

(faut téléphoner au Guiness)

mardi, octobre 14 2008

berk, berk, berk

int  lm_match(struct lm_softc *);
int  wb_match(struct lm_softc *);
int  def_match(struct lm_softc *);

struct lm_chip {
        int (*chip_match)(struct lm_softc *);
};

struct lm_chip lm_chips[] = {
        { wb_match },
        { lm_match },
        { def_match } /* Must be last */
};

void
lm_attach(struct lm_softc *sc)
{
        u_int i, config;

        for (i = 0; i < sizeof(lm_chips) / sizeof(lm_chips[0]); i++)
                if (lm_chips[i].chip_match(sc))
                        break;

J'ai remis bout à bout les bons morceaux, dans la réalité, c'est bien plus espacé, sinon se serait trop lisible... (on remarquera que j'ai tout de même laissé les commentaires d'origine)