embedded weblog

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

vendredi, mars 12 2010

dolosif arrière

Une technique très SSIIène consiste en la vente d'un projet sous-évalué, puis en la signature d'avenants au fur-et-à-mesure, surtout quand le client est dos au mur et ne peut plus reculer, qui s'accumulent jusqu'à faire exploser le budget initial. Technique célèbre, qui est évidemment le mal. Ne serait-ce que parce que la réputation du métier en pâtis. On pourrait d'ailleurs se demander si c'est la baisse des tarifs acceptables par les clients (longtemps plumés, dans les années 80/90 -- si vous saviez combien a coûté le site web de Radio France, et encore pas la totalité, c'est à pleurer -- c'est que ce sont nos impôts...) qui a entrainé ce genre de techniques contestables pour se rattraper (faire croire que ce n'est pas cher alors que oui, ça l'est vraiment), ou si c'est la concurrence entre sociétés proposant du service informatique, tirant toujours plus bas les prix (et donc les salaires -- problème connexe : un ingénieur mal payé est un ingénieur démotivé, faudrait-il toujours se rappeler ; et un ingénieur démotivé travaille plus lentement et produit plus d'erreurs), qui a pour effet de fausser ladite concurrence en annonçant des prix qui ne sont pas les bons dès le départ. Ou peut-être est-ce dû à une mauvaise émulation des deux phénomènes.

C'est en tout cas une dérive clairement constatée, qui en fait part d'un fait tout bête : les engagements contractuels portant sur du résultat (et non du moyen) nécessitent une estimation et une vision qui est rarement la bonne dès le départ (évidemment on fait ce que l'on peut, mais la boule de cristal est hors de prix chez Swarovsky® !), et qui est donc sujette à évolution (sachant que les cas litigieux ne sont pas rares : j'ai un ami informaticien fraichement retraité qui ne s'occupait plus que de ça à la fin de sa carrière, notamment sur la ligne 14 Meteor du métro parisien, où ça a pas mal chauffé paraît-il) ; on sait aussi que l'on fait généralement son beurre sur les avenants (ne serait-ce que parce que la phase d'avant-vente est inutile ou réduite à son maximum, et que l'expertise à faire valoir sur le produit est évidente). Il n'empêche que les (très) gros projets peuvent déraper, et ce de manière spectaculaire.

Rappelons encore : l'ingénieur a un devoir de conseil, et a fortiori la société qui l'embauche. Question de bonne ou de mauvaise foi. Les 11 millions d'Euros de condamnation d'IBM (première instance, appel évidemment interjeté, mais en tout cas une exécution provisoire ordonnée, donc les 11 millions doivent être versés, avec tout ce que ça implique en terme de trésorerie) tendraient à mettre un frein sérieux à ce genre de pratiques, qualifiées "manoeuvres dolosives" (rappelez-vous de vos cours de droit, différence dol/vol -- le dol est la tromperie, la fraude, ce qui implique d'avoir su dès le départ mais de n'avoir rien dit pour en tirer profit, dans notre affaire). C'est sûr que doubler une facture de 7,3 millions et le temps de réalisation en cours de route, avec tout un tas d'inconvénients très impactant sur la société cliente, ça fait vraiment mal. À noter aussi que la MAIF s'est plainte au TGI, et non au tribunal de commerce (je me demande pourquoi, en fait, puisque Niort en possède un ; va falloir que je demande à notre juriste). Je partage en tout cas l'analyse de l'experte de 01 (encadré tout en bas).



Au passage, un peu de linguistique historique, histoire d'expliquer le titre (il me semblait bien que le Littré n'allait pas au bout des choses entre dolus pour dol, et dolorem pour douleur -- quelqu'un a un Alain Rey sous la main ?) :

En latin classique, la notion de deuil fait partie du sémantisme de dolor. Cicéron, de Oratorio, 2, 199 : "Par mon discours, je ravivais la douleur (dolorem), de ceux qui avaient à pleurer des parents."
Ainsi dolus est une simple variante de dolor. Pourquoi apparaît-il ? Cela s'explique probablement par des considérations formelles et sémantiques :
- forme : le paradigme de dolor renferme une forme ambiguë : le génitif pluriel dolorum. Une réinterprétation permet le changement à dolus.
- sens : il y a des exemples limites : dolus : mal, malice; mal s'apparente à douleur.
L'exemple ambigu remonte au Ier ap., dans la Thébaïde de Stace, 5, 117-119, qui fait allusion aux crimes des Danaïdes qui tuèrent leur mari avec la complicité de leur père qualifié de "la‘tus (riche) dolorum (ruse et deuil)." La ramification au dédoublement lexical se produit à l'époque du latin tardif. La comparaison des langues romanes joue un rôle. L'histoire d'une langue particulière s'appuie aussi sur l'histoire des langues du même groupe.

jeudi, mars 11 2010

acceptable values

}

static int
dacmdsizesysctl(SYSCTL_HANDLER_ARGS)
{
    int error, value;

    value = *(int *)arg1;

    error = sysctl_handle_int(oidp, &value, 0, req);

    if ((error != 0)
     || (req->newptr == NULL))
        return (error);

    /*
     * Acceptable values here are 6, 10, 12 or 16.
     */
    if (value < 6)
        value = 6;
    else if ((value > 6)
          && (value <= 10))
        value = 10;
    else if ((value > 10)
          && (value <= 12))
        value = 12;
    else if (value > 12)
        value = 16;

    *(int *)arg1 = value;

    return (0);
}

static cam_status
daregister(struct cam_periph *periph, void *arg)

Au moins, ça ne manque pas d'originalité. Je vous ai mis ce qu'il y autour afin de bien se rendre compte du vide intersidéral de commentaires utiles autour de ce fabuleux effet de bord sur une variable récupérée par macro, et transvasée de manière bien folklorique (dommage que le traitement ne soit pas fait par masques de bits, tout de même !). Autour de la ligne 1010 du driver SCSI de freebsd.

vendredi, mars 5 2010

le vendredi c'est permis

The $5 Guerrilla User Test est un article proprement hilarant mais on ne peut plus sérieux : il expose une manière simple de tester ses créations logicielles sans avoir à embaucher un crétin, ou quelqu'un qui "pense crétin" afin de rendre le programme "idiot-proof" "user-proof". L'idée de base est qu'en réalité, l'utilisateur n'est pas stupide, il est juste distrait en permanence, et ne porte pas attention à ce qu'il fait (OK, il y en a qui sont vraiment stupides, mais gardons foi en l'humanité -- c'est notre vivier de clientèle potentielle après tout). Embaucher un fonctionnel pensant stupide n'est donc pas la bonne solution : la seule chose que cela va créer, c'est une interface ultra-simplifiée qui ne fera pas ce qui est attendu, et deviendra à terme une insulte à l'intelligence (ça me rappelle...). En revanche, le "$5 guerrilla user test" propose d'aller voir de vrais gens de tous les jours, mais dans un état entre la concentration et la distraction.

Pour cela, il suffit d'aller dans le bar d'en face (pour nous : le Washington ou le Bowler ; faudra se re-renseigner après le déménagement), et d'aller vers la bonne personne qui semble ouverte pour tester le logiciel (l'article propose tout un tas d'astuce pour repérer les bonnes personnes -- personnellement j'aurais plutôt tendance à aller vers la gent féminine, mais je n'aime pas beaucoup les blondes), pour lui proposer une bière en l'échange du test (d'où les 5$ -- notez que la bière vient en récompense après, logique, ce qui implique de choisir une personne déjà "installée"). La technique d'approche est aussi longuement exposée (retours d'expérience en commentaires, aussi). Les résultats sont paraît-il exceptionnels. À quand des sessions de Linshare ou d'OBM dans les bars ? (on pourrait installer une distributeur de bières sur le stand de solution Linux, sinon... Je suis sûr d'emporter l'adhésion de beaucoup de geeks linagoriens)


disclaimer: l'auteur de ce billet ne boit que du lait fraise ; à consommer avec modération, l'abus de génie logiciel est mauvais pour la santé

update 10 minutes plus tard: on me fait déjà remarquer qu'en France, surtout à Paris, ce serait plutôt autour de 8€ le test (le cours de la bière est bien supérieur)

mercredi, mars 3 2010

money drain

Très bon article des échos : "Start-up informatiques : razzia sur les pépites". Après le brain drain qui prive le pays de ses brillants cerveaux (sur mon nouveau compte linkedIn, je vois du Citrix à Cambridge, du VMWare à SF, mais je connaissais déjà du Microsoft à Redmond, et j'en passe -- c'est d'ailleurs comme ça, à mon avis, que la promotion de Génie Industriel de l'EPITA a surpassé en terme de salaires les autres filières, il ne faut pas se le cacher : allez simplement voir les salaires de techniques chez ARM outre-manche), tandis que c'est l'État qui paie la formation, l'inquiétude peut légitiment se porter sur les multiples rachats de start-ups françaises, par les américains principalement. C'était le cas d'ailleurs de la société d'un ami, dans la téléphonie mobile, par Microsoft (c'est lui qui bosse à présent à Redmond). Ça été aussi le cas de Trango Virtual Processor, où j'ai travaillé, par VMWare ; et avant ça de Mobivillage, où j'ai été stagiaire, par un groupe géant japonais (en 2004, pourtant, mais la téléphonie mobile, c'est impressionnant là-bas). L'année dernière, on notait sur le salon RTS le rachat de Polyspace par Mathworks (notons qu'avec un nom anglophone, ça facilite d'autant la mondialisation -- et ses effets secondaires de "concentration"). Quant à Business Object... (couic, dans l'oeuf !)

Des start-ups, j'en connais aussi. Et l'investissement par levée de fonds, je connais indirectement, soit en observant du côté des entreprises, soit en discutant avec l'un de mes amis (qui effectivement investit encore plus depuis la loi TEPA). Généralement, c'est un gouffre : des millions d'Euros en entrée, pas grand chose en sortie. Pourtant, il faut bien évoluer, l'innovation est une absolue nécessité. J'avoue ne pas bien voir comment résoudre la situation. Un pacte PME acté par une loi ("investissement" des grandes société obligatoire dans les petites) peut effectivement sembler pertinent (après tout, on se bat pour des bouts de ficèles quand les grands groupes dégagent des bénéfices extraordinaires, sur des marchés souvent étrangers ou publics...). À entendre Daniel Glazman, par exemple (disruptive innovation : in English, again), les formalités, le poids de l'administration, entravent grandement la productivité (et donc l'innovation) seule (d'un autre côté, c'est notre identité nationale !). J'avoue être assez pessimiste d'une manière générale sur l'entrepreneuriat à l'heure actuelle ; d'ailleurs je pense que toutes les sociétés rachetées ne faisaient pas un sou de bénéfice (ah, les levées de fonds à 2 ou 4 millions d'Euro, malgré la collection de projets estampillés "Européens" ou les CIR à en pleuvoir...). Vraiment, je ne vois pas. Mais de la technologie qui s'en va à l'étranger (même en restant sur le sol national), ce n'est jamais très bon signe.

(c'est quelque part une sécurité à Linagora : je sais qu'on ne se vendra jamais, question d'hommes)

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.

lundi, janvier 25 2010

commentaire expiatoire du jour

Il ne faudrait pas que cela devienne une habitude, mais comment m'empêcher, chers lecteurs, de vous faire partager ce commentaire d'excuse d'une mauvaise foi évidente ? (trouvé là)

GHashTable *gaim_dbus_iter_hash_table(DBusMessageIter *iter, DBusError *error) {
    GHashTable *hash;

    /* we do not need to destroy strings because they are part of the message */
    hash = g_hash_table_new(g_str_hash, g_str_equal);

    do {
        char *key, *value;
        DBusMessageIter subiter;

        if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY)
            goto error;         /* With all due respect to Dijkstra,
                                   this goto is for exception
                                   handling, and it is ok because it
                                   avoids duplication of the code
                                   responsible for destroying the hash
                                   table.  Exceptional instructions
                                   for exceptional situations. */

        dbus_message_iter_recurse(iter, &subiter);
        if (!gaim_dbus_message_iter_get_args(&subiter, error,
                                             DBUS_TYPE_STRING, &key,
                                             DBUS_TYPE_STRING, &value,
                                             DBUS_TYPE_INVALID))
            goto error;         /* same here */

        g_hash_table_insert(hash, key, value);
    } while (dbus_message_iter_next(iter));

    return hash;

 error:
    g_hash_table_destroy(hash);
    return NULL;
}

add: Et juste en dessous, le scandale :

#include "dbus-bindings.c"

void *gaim_dbus_get_handle(void) {
    static int handle;

    return &handle;
}

lundi, janvier 18 2010

le code geekesque du jour

static void
to_be_or_not_to_be (void)
{
/*
* If all of the options that control our policy are disabled, then we
* have no point in living. Save the user some memory and exit.
*/
/* you used to say live and let live... */
gboolean live = FALSE;
size_t i;

/* ...but in this ever changing world in which we live in... */
for (i = 0; i < G_N_ELEMENTS (gvm_settings) && !live; i++) {
if (gvm_settings[i].type == TYPE_BOOL)
live = *((int *) gvm_settings[i].var);
}

/* makes you give it a cry... */
if (!live) {
dbg ("daemon exit: live and let die\n");
exit (EXIT_SUCCESS);
}
}

Toi aussi, amuse-toi à compter les références dans ce morceau de code...

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...

vendredi, octobre 23 2009

oui, non, 42

PgAdmin, version windows (oui, je sais, mais le client est roi -- oui, je sais aussi), m'afficha un jour ce message :

Je ne compris point sa portée jusqu'à ce message de windaube 2003 serveur, alors que je faisais un glisser-déplacer depuis le gestionnaire de fichiers vers le bureau :

Voilà un abyme de perplexité dans lequel je fus sans ménagement jeté. Oui, non ? 42 !

(il semblerait qu'il faille interpréter ce message selon : "je veux copier/déplacer" / "je ne veux pas" ; et qu'on ne vienne pas me demander à quoi ça sert !)

mercredi, octobre 21 2009

encart publicitaire

Vos produits éditoriaux en « 24h » et « 4 à 8 jours » seront expédiés en une seule fois entre le 29/10/2009 et le 31/10/2009.

Votre site web a la polio (par exemple, un bouton "ajouter" qui fait un submit sur plusieurs champs dont certains cachés puisque sur des onglet en javascript, de telle sorte que si l'on s'était trompé de champs et que l'on en a rempli plusieurs avant de cliquer sur le bouton, il ne se passe rien de bon, tout en restant silencieux), et vous avez besoin d'aide pour sa refonte ? On peut vous aider...

(autrement, il y a des prestataires, parfois, qui ne méritent pas de s'approcher à moins de 200 mètres d'un ordinateur)

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 donc le service formation de Linagora, je suis persuadé que c'est arrangeable.  ;)



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\n", 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...

mercredi, avril 15 2009

no comment

Vista today, post-Service Pack 2, which is now in the marketplace, is the safest, most reliable OS we've ever built. It's also the most secure OS on the planet, including Linux and open source and Apple Leopard. It's the safest and most secure OS on the planet today.

Remarks by Kevin Turner, chief operating officer for Microsoft, about the role of CIOs in a changing economy
MidMarket CIO Summit
Redmond, Wash.
April 6, 2009

lundi, mars 23 2009

un troll peut cacher bien des vérités

Cela faisait longtemps que je n'avais point trouvé sujet à troll, et même si ce n'est pas vendredi, voici un billet sur une pile d'appel Java pour le moins... impressionnante. Même en ruby, on ne fait pas mieux, même si c'est déjà beaucoup. D'autant que nous ne somme pas encore dans le code de l'interpréteur. Cependant, il faut paraît-il minorer pour le Java : la pile présentée est celle "virtuelle" (correspondant aux sources), et non la "réelle" qui peut être bien plus optimisée ; on n'en demeure pas moins songeur.

Faut-il crier haro sur le Java pour autant ? (je rappelle que nous sommes sur un blog embarqué) Un ingénieur très expérimenté en informatique industrielle/embarquée (ça tombe bien) dit que non. Et il détaille très longuement. J'aurais tendance à appuyer cet argumentaire, mais constate que l'Ada est tout de même plus sympathique que le Java (même si les effets de bords y sont bien plus aisés, par exemple parser des chaînes de caractères en Ada est à la limite de la calamité). En attendant, chacun sait mon aversion pour le C (d'autant que je le maîtrise parfaitement, sinon je ne pourrais en faire état :)  ). De même, des implémentations "solidifiées" de Java se comptent au nombre de deux (au moins, il faut voir où en est Sun aussi) depuis bien des années maintenant, et je n'ai pas l'impression qu'elles suscitent un grand engoument ; on vérifiera cela sur le stand d'Aonix (qui n'hésite pas à comparer Ada et Java) au prochain salon RTS.

Vers la fin de cet argumentaire très intéresant que j'ai cité, on trouve ceci :

One of the biggest lessons I've learned in my many years of working, interviewing, and managing projects is that specific technical skills are far less important than outlook and character. Technical skills are the easiest skills to teach. They're also the skills that must be updated and replaced regularly.

Le hasard voulait que je tienne précisément le même discours fort récemment, ce week-end pour être précis. Le problème est alors l'enseignement de cette partie très importante du métier de l'ingénieur (à tel point que l'évolution de carrière tend à en faire l'unique sujet, alors que la technique "pure" disparaît du champ d'action à plus ou moins court terme) : pour moi, cette formation de l'esprit passe par du retour d'expériences à travers de la veille technologique et de l'étude de marché (mêler des cas réels à des graphes d'études), par exemple. Simplement, ça ennuie toujours les étudiants...  :)

jeudi, février 12 2009

la consternation ksh

Mais quel est ce comportement ?

There is one restriction on integer variables. Once a variable is set to integer type, it can't be assigned a non-integer value:

$ typeset —i NUM=abc 
/bin/ksh: NUM: bad number

Je ne sais pas bien si c'était à la base une idée de typer a posteriori des variables non-typées a priori, mais dans tous les cas ça pue déjà parce que pas standard comme comportement, et ensuite c'est loin d'être la meilleure idée du siècle dans l'absolu (si le langage est pourri, autant y aller jusqu'au bout !). Mais le pire du pire, c'est que l'erreur renvoyée est aussi parlante que :

script[167]: 08: bad number `08'

Hors, 167, ici, c'est un numéro de ligne qui correspond à la fin du "if" dans lequel on se trouve (if qui fait bien 40 lignes...). Tout à l'heure, j'ai vu la même erreur avec "09", ça arrive une fois toutes les deux heures en test (pour une exécution toutes les 30 secondes), sachant que ça pourrait effectivement bien être dû à une arithmétique de dates. Quelques recherches sur internet montrent que bien du monde s'est arraché les cheveux dessus. Heureusement, la solution était au milieu de tout ça...

Comme d'habitude, on retiendra qu'il faut fuir ksh. Mais décidément, les systèmes UNIX [pré-]historiques en raffolent toujours.

Edit: raté ! C'était :

        case ET_BADLIT:
                warningf(true, "%s: bad number `%s'", es->expression, str);
                break;

dans ksh/expr.c (on remarque les backquotes dans l'erreur). Mais ça ne m'avance guère plus, à vrai dire... (il semble que ce soit bien dans une assignation de variable, en tout cas)

Ré-édit:

Osanna pour Patrick ! (qui a déjà dû subir la même erreur en son temps)  Ah bien c'est que "0" suivi d'un chiffre, c'est considéré comme de l'octal, et justement :

struct tbl *
setint_v(struct tbl *vq, struct tbl *vp, bool arith)
{
        int base;
        long num;

        if ((base = getint(vp, &num, arith)) == -1)
                return NULL;

Sauf que j'avais pas capté (ça m'apprendra à tester avec $((04)) pour une erreur de "08" ou "09" !). Alors, la solution en bois sera de concaténer un "1" au début de la chaîne, et d'enlever 100 ensuite dans l'opération : berk !!

Oh, et ksh est quand même bien pourri dans ses messages d'erreur :

# echo $((4+08))
ksh: 4+08: bad number `08'
# echo $((08))
ksh: 08: bad number `08'
# HOUR=12
# MIN=03
# SEC=08
# TIME=$((HOUR*3600+MIN*60+SEC))
ksh: 08: bad number `08'
# SEC=03
# TIME=$((HOUR*3600+MIN*60+SEC))
# echo $TIME
43383

jeudi, décembre 18 2008

conférence Paris8 (mais cette fois, ce n'était pas moi)

Non, c'était Pierre Gronlier, un plus-que-fort bon ami de mes années d'étude à l'EPITA, qui après avoir brillamment obtenu son diplôme spécialisé dans le logiciel embarqué et temps-réel (je précise, parce qu'on était classé au même niveau, donc je m'envoie indirectement des fleurs), a continué très originalement sur un master à l'ENS Cachan. Spécialité : le traitement de l'image. Tout un programme !

Coopté lors de ma propre conférence auprès de la gente Isis Truck -- qu'il est toujours un plaisir de revoir --, je peux me targuer d'avoir aussi recommandé Basile Starynkevitch pour le 26 janvier prochain, comme quoi je devrais penser à me reconvertir en agence (ça dépendra de mon EAD, hum :D ) ; en revanche, je ne suis pour rien dans la programmation de Loïc Dachary du 2 février suivant, mais le monde est décidément petit. Ce qui est certain, c'est que pour une première expérience, ce fut fort bon. La conférence a porté sur le codage vidéo (et non l'encodage, pensons aux apparatchik de l'orthographe françoise, même si la subtilité est tellement subtile que...), car Pierre travaille pour Actimagine. Le lien avec l'embarqué est évident : le but premier est de permettre une décompression vidéo de leur propre format, Mobiclip, équivalent en qualité au h264, sur des appareils déjà en circulation et non prévus pour (CPU pas assez puissant et GPU absent), ou disposant de peu de batterie : téléphones, consoles vidéos, etc. Bon, c'est pas libre, mais franchement, la démonstration est assez bluffante pour ne pas dire que ça pue, à moins d'être malhonnête (et puis, il y a aussi des choses pas libres qui puaient, et qui une fois libérés sentent toujours fort, Java par exemple, et le reconnaître vous empêche d'être qualifié d'ayatollah du libre, alors...). En revanche, aussi excellent soit le produit, il serait bien de penser à une version Linux autre que celle de l'ami Pierre sur son PC. Bref...

Pierre a parlé d'à peu près tout, et de fait ça a duré : après avoir galéré sur la ligne 13 et être arrivé à 18h20 (18h00 en prévision, c'est tôt, mais bon...), la présentation a terminé bien après 20h00 (une bouteille entière d'une célèbre boisson désaltérante fut sacrifiée). Différences de format de câbles, d'affichage, de codecs, subtilités afférentes au codage vidéo -- le gamma et ses histoires ubuesques, entre autres héritages-boulets que l'on traîne toujours --, on aura même parlé de la perception visuelle avec une plongée dans notre capteur oculaire portatif (qui peut cependant aussi servir à la drague). Comme les slides sont disponibles, il est inutile de s'étendre.

Mais louons tout de même cette forte introduction-et-plus à ce sujet plus passionnant que ça en a l'air de prime abord (quoique les cours de traitement de l'image n'étaient pas mes préférés, j'ai toujours préféré les manchots en boîte), et surtout que l'on peut être amené à croiser un jour ou l'autre : outre la petite digression dans l'échec de la DVB-H en France (et il se trouve que j'ai bossé sur l'implantation de la DVB-SH : mais en l'occurrence, le simulateur de communication satellite avec le récepteur-retransmetteur terrestre), la partie sur la transmission télévisuelle m'aura rappelé mon passage dans les services de Philips l'année dernière (aujourd'hui racheté par Pace), où j'ai dû ingurgité dans le métro les bases du métier, avec le très bon ouvrage maison (on y parle beaucoup d'OpenTV, de fait, qui comme l'indique son nom est très fermé -- c'est ce qui fait tourner C+, par exemple). Alors il est toujours bon, pour sa culture, de savoir de quoi l'on parle. Surtout lorsque les STB, les télés, les lecteurs DVD et j'en passe migrent tous peu à peu sous Linux (et même les retransmetteurs de DVB-SH, donc...).

J'invite donc tout un chacun à découvrir les 10Mo de slides de Pierre, que je peux encore remercier au passage.

mercredi, décembre 10 2008

les comportements erratiques de syslog-ng

* un fichier sans ligne "destination remote" implique une impossibilité de démarrer syslog-ng (avec une erreur franchement pas belle, ça râle)
* un fichier avec un "destination remote { };" garde les logs uniquement en local
* un kill -HUP envoyé sur un syslog-ng relit le fichier de conf et le prend en compte (sans changer le PID, mais un changement de filtre en plus permissif montre bien que ça été pris en compte)

Soit un fichier avec un "destination remote" avec ou sans IP à l'intérieur permet de démarrer le syslog-ng sans problème. On enlève la ligne "destination remote". On envoie un -HUP. Question : que se passe-t-il ? Eh bien rien du tout. Encore un bug non détecté... (pour le cas où l'on démarre le démon à la main, ce qui en l'occurrence n'arrivait qu'au démarrage, et pour un fichier sans adresse IP de serveur distant : il fallait deviner que ça ne voulait pas démarrer sans !)

- page 1 de 2