KMap - un outil d'Image Mapping pour KDE

Capture écran
KMap - 2.0 - (Image Mapping Tool) -

Ce petit logiciel, dont la première version remonte à 2001/2002 sous Qt2, est un outil d'Image Mapping (cartographie web). Je l'ai développé à l'époque pour détourer les cartes des arrrondissements, cantons et communes du Haut-Rhin, encore visibles aujourd'hui sur le site internet du Centre Départemental d'Histoire des Familles à Guebwiller.

Bien avant l'arrivée de Google Maps et Cie, dans un temps reculé ou le web était extrêmement lent (modem 56 Kbps), et les navigateurs bien plus limités qu'aujourd'hui, l'Image Mapping permettait (et permet toujours) de segmenter une image en zones cliquables (rectangles, cercles et polygones), en attribuant à chaque zone des attributs HTML distincts (lien internet, évènements DOM, etc.).

Il est clair qu'avec les outils web actuels, ce logiciel est un peu désuet par sa fonction, mais pour qui veut étudier la librairie Qt, et comprendre notamment le fonctionnement du dessin sous KDE, il reste tout à fait d'actualité ! C'est également pour cette raison que j'ai préféré laisser les commentaires via qDebug dans le code, ce qui fera sûrement hurler les puristes, mais tant pis ou tant mieux !

La version ici présentée est une réactualisation de 2012 pour la librairie Qt4.

>> TÉLÉCHARGER <<

Schéma de l'application

                           Point (point)
                                 |
                                 v
                           Map (carte)
                                 |
                                 v
                          Canvas (dessin)
                                 ^
                                 |
                          Kmap (contrôle) <- FormFieldSet (attributs HTML)
                                 ^
                                 |
                  MainWindow (fenêtre principale)
                                 ^
                                 |
                        main.cpp (lancement)

Schéma d'étude

Nous commencerons ici par étudier les classes Point, Map et FormFieldSet, qui sont assez simples. Nous continuerons avec main.cpp, et MainWindow qui définit la fenêtre principale de l'application. Enfin, nous aborderons la classe Kmap, qui est le widget principal (MainWidget) de la fenêtre principale (MainWindow), pour finir par le plus dur : la classe Canvas, qui est le coeur réel du programme.

Le code étant long, et déjà assez complexe, les thermos de café sont autorisées !

point.h

Notre application ayant pour but de créer des cartes 2D, il nous faut en tout premier lieu des points. Fort heureusement, Qt propose déjà un objet QPoint, dont il serait bien dommage de se priver !

#ifndef POINT_H
#define POINT_H

#include <QPoint>
#include <stdlib.h>

#include "map.h"
class Map;

class Point {
public:
	Point( Map * _parent, QPoint p );
	~Point();
	Map * _parentmap;
	QPoint xy;
	/** show coordinates */
	QString show ();
	/** return true if point pt is in rectangle defined by p0 and p1 */
	bool inRectangle ( QPoint p0, QPoint p1 );
	/** return true if point pt is in circle defined by center p0 and radius p1-p0 */
	bool inCircle ( QPoint p0, QPoint p1 );
	/** return true if pt is in ellipse/rectangle defined by p0 and p1 */
	bool inEllipseRectangle ( QPoint p0, QPoint p1 );
	/** return true if pt is in ellipse defined by center p1 and radius p1-p0 */
	bool inEllipseCenterRadius ( QPoint p0, QPoint p1 );
	/** return true if the current point is inside the map */
	bool inPolygon ( Map * _map );
};

#endif

point.cpp

Chaque point est relié à une seule et unique carte parente.

Outre le constructeur qui initialise un objet QPoint, la majorité des fonctions de ce fichier servent uniquement à tester si un point est dans une forme donnée (rectangle, cercle, ellipse ou polygone).

Pour le polygone, qui est le cas le plus complexe, j'ai repris l'algorithme de Bob STEIN, paru dans le Linux Journal du 17 février 2000.

A noter enfin la syntaxe particulière de la QString s dans la méthode Point::show(). Cette nouvelle notation de Qt4 permet un équivalent du sprintf classique, mais en beaucoup plus souple.

#include "point.h"

Point::Point( Map * _parent, QPoint p ) {
	_parentmap = _parent;
	xy = p;
}

Point::~Point() {
}

/** show coordinates */
QString Point::show () {
	QString s = QString( "(%1,%2)" ).arg(this->xy.x()).arg(this->xy.y());
	return s;
}

/** return true if point pt is in rectangle defined by p0 and p1 */
bool Point::inRectangle ( QPoint p0, QPoint p1 ) {
	QPoint pt = this->xy;
	bool inside;
	inside = false; // we suppose to be out of the rectangle
	int x0, y0, x1, y1;
	if ( p0.x() > p1.x() ) {
		x0 = p1.x(); x1 = p0.x();
	} else {
		x0 = p0.x(); x1 = p1.x();
	}
	if ( p0.y() > p1.y() ) {
		y0 = p1.y(); y1 = p0.y();
	} else {
		y0 = p0.y(); y1 = p1.y();
	}
	if ( x0 <= pt.x() && pt.x() <= x1 && y0 <= pt.y() && pt.y() <= y1 )
	 inside = true;
	return inside;
}


/** return true if point pt is in circle defined by center p0 and radius p1-p0 */
bool Point::inCircle ( QPoint p0, QPoint p1 ) {
	QPoint pt = this->xy;
	bool inside;
	inside = false; // we suppose to be out of the circle
	QPoint p2;
	p2 = p1 - p0;
	int r;
	r = ( abs( p2.x() ) > abs( p2.y() )) ? ( abs( p2.x() ) ) : ( abs( p2.y() ) );
	int x0, y0, xt, yt;
	x0 = p0.x(); y0 = p0.y(); xt = pt.x(); yt = pt.y();
	if ( (xt-x0)*(xt-x0)+(yt-y0)*(yt-y0) <= r*r )
		inside = true;
	return inside;
}

/** return true if pt is in ellipse/rectangle defined by p0 and p1 */
bool Point::inEllipseRectangle ( QPoint p0, QPoint p1 ) {
	QPoint pt = this->xy;
	bool inside;
	inside = false; // we suppose to be out of the ellipse
	if ( this->inRectangle( p0, p1 ) ) {
		int x0, y0, x1, y1;
		if ( p0.x() > p1.x() ) {
			x0 = p1.x(); x1 = p0.x();
		} else {
			x0 = p0.x(); x1 = p1.x();
		}
		if ( p0.y() > p1.y() ) {
			y0 = p1.y(); y1 = p0.y();
		} else {
			y0 = p0.y(); y1 = p1.y();
		}
		if ( x0 == x1 || y0 == y1 ) { // rectangle is a line...
			return true;
		}
		int xt, yt;
		xt = pt.x(); yt = pt.y();
		float xc, yc, a, b;
		xc = (x0+x1)/2; yc = (y0+y1)/2;
		a = (x1-xc); b = (y1-yc);
		if ( ((xt-xc)*(xt-xc))/(a*a)+((yt-yc)*(yt-yc))/(b*b) <= 1 )
			inside = true;
	}
	return inside;
}

/** return true if pt is in ellipse defined by center p1 and radius p1-p0 */
bool Point::inEllipseCenterRadius ( QPoint p0, QPoint p1 ) {
	bool inside;
	QPoint p2;
	p2 = p0 - ( p1 - p0 );
	inside = this->inEllipseRectangle( p2, p1 );
	return inside;
}

/** return true if the current point is inside the map */
bool Point::inPolygon ( Map * _map ) {
	QPoint pt = this->xy;
	QPoint p1, p2, pold, pnew;
	long x1, y1, x2, y2, xt, yt;
	bool inside;
	xt = (long)pt.x(); yt = (long)pt.y(); // point to test
	inside = false; // we suppose to be out of the polygon
	// we will begin at last point of polygon
	pold = _map->_points.at( _map->_points.count()-1 )->xy;
	// if we have three points, we must check three segments
	for ( int i=0; i<int( _map->_points.count() ); i++ ) {
		pnew = _map->_points.at(i)->xy;
		if ( pnew.x() > pold.x() ) { // always p1 to left and p2 to right
			p1 = pold; p2 = pnew;
		} else {
			p1 = pnew; p2 = pold;
		}
		// algorithm of Bob STEIN founded in
		// Linux Journal February 17, 2000

		// explanation :
		// the purpose is to take the tested point,
		// to build a segment starting at this point and going to north
		// and see how many times the segment will cut the polygon.
		//
		// we study only the segment p1,p2 excluding point pold
		// the current point must be in the segment or , if segment is vertical,
		// the inside will be count only for a point, not twice...
		//
		// the second condition is a dot product for [01,02] and [01,0t].
		// According to the fact we travel to the north pole, this product must be negative.
		// to count the point inside the polygon
		x1 = (long)p1.x(); y1 = (long)p1.y(); x2 = (long)p2.x(); y2 = (long)p2.y();
		//printf("%d : (%d,%d)(%d,%d) (%d,%d) (p1.x()<xt)=%d (xt<=p2.x())=%d (yt-y1)*(x2-x1)=%d (y2-y1)*(xt-x1)=%d\n",
		//(inside ? 1 : 0 ),xt,yt,x1,y1,x2,y2,p1.x()<xt,xt<=p2.x(),(yt-y1)*(x2-x1),(y2-y1)*(xt-x1));
		if ( ( p1.x() < xt ) == ( xt <= p2.x() )
			&& (( yt-y1 ) * ( x2-x1 )) < (( y2-y1 ) * ( xt-x1 )) )
				inside = !inside;
		// prepare next segment
		pold = pnew;
	}
	return inside;
}

map.h

Nous avons déjà les points, passons aux cartes, constituées de points.

Chaque carte aura pour attributs un type (rectangle, cercle, ellipse ou polygone ) et une couleur.

#ifndef MAP_H
#define MAP_H

#include <QList>
#include <QColor>
#include <QHash>
#include <QDebug>

#include <math.h>

#include "point.h"
class Point;

#include "canvas.h"
class Canvas;

struct DoublePoint {
	double x;
	double y;
};

extern QString htmllabel[];
extern int nblabels;

class Map {
public:
	Map( char maptype, QColor mapcolor );
	~Map();
	/** read a map using key=values stringlist */
	bool readFromHtml( char t, QString args );
	/** translate map to HTML */
	QString convert2Html ();
	/** Return html options to hash */
	QHash<QString, QString> getAttributes2Hash ();
	/** Return html options to string */
	QString getAttributes2String ();
	char type; // cf. enumerated types R C E L P
	QColor color; // map color
	QList<Point *> _points; // map points
	bool isgrabbed; // true if current map is grabbing
	bool ismoved; // true if current map is moving
	QStringList htmlfields; // HTML elements
private:
	/** convert ellipes to polygon */
	QString ellipse2polygon ( double xcenter, double ycenter, double xradius, double yradius );

signals: // Signals
	/** show formfieldset */
	void signalShowFormFieldSet( int i );
};

#endif

map.cpp

Outre le type et la couleur de la carte, l'objet initialise deux variables d'état (flags) et un tableau.

Les variables d'état reflètent les opérations de dessin en cours :

  • La variable isgrabbed permet de savoir si la carte a été sélectionnée pour une opération hors dessin (copie, effacement, changement de couleur, ...), ce qui permet par la suite de grouper plusieurs cartes ensemble et de leur appliquer une opération.
  • La variable ismoved est vraie si un point de la carte est en train d'être déplacé.

Le tableau htmlfields contient les balises HTML de la carte (HREF, STYLE, etc.).

On remarquera également que map.cpp gère la convertion des cartes au format HTML en lecture/écriture.

La méthode ellipse2polygon mérite aussi une certaine attention. Seuls les objets de type cercle, rectangle et polynômes sont gérés nativement en Image Mapping web. On est donc obligé de convertir les ellipses en polygones, ce qui engendre une petite perte de précision lors des exports HTML/XML notamment.

#include "map.h"
Map::Map( char maptype, QColor mapcolor ) {
	type = maptype;
	color = mapcolor;
	isgrabbed = false;
	ismoved = false;
	for ( int i=0; i<nblabels; i++ )
		htmlfields.append("");
}

Map::~Map() {
}

/** convert ellipses to polygon */
QString Map::ellipse2polygon ( double xcenter, double ycenter, double xradius, double yradius ) {
	const int res = 32; // resolution : 4*32 = 128 points
	DoublePoint p[4*res];
	QString s;
	double x=0., y=0.;
	int i;
	double a = abs(xradius);
	double b = abs(yradius);
	xcenter = abs(xcenter);
	ycenter = abs(ycenter);
	qDebug() << "ellipse2polygon: center(" << xcenter << ";" << ycenter << ") xradius=" << a << " yradius=" << b;
	if (!a && !b )
		return "1,1,1,1"; // null ellipse ?
	// record the first points
	for ( i=0; i<=res; i++ ) {
		if ( a > b ) {
			y = (b*i) / res;
			x = a * sqrt( 1 - (y*y)/(b*b) );
		}
		if ( a <= b ) {
			x = (a*i) / res;
			y = b * sqrt( 1 - (x*x)/(a*a) );
		}
		p[i].x = x;
		p[i].y = y;;
		qDebug() << "i=" << i << " x=" << p[i].x << " y=" << p[i].y;
	}
	if ( a > b ) {
		for ( i=1; i<=res; i++ ) {
			p[res+i].x = -p[res-i].x;
			p[res+i].y = p[res-i].y;
			qDebug() << "i=" << i << " x=" << p[res+i].x << " y=" << p[res+i].y;
		}
		for ( i=1; i<=res; i++ ) {
			p[res*2+i].x = p[2*res-i].x;
			p[res*2+i].y = -p[2*res-i].y;
			qDebug() << "i=" << i << " x=" << p[2*res+i].x << " y=" << p[2*res+i].y;
		}
		for ( i=1; i<res; i++ ) {
			p[res*3+i].x = -p[3*res-i].x;
			p[res*3+i].y = p[3*res-i].y;
			qDebug() << "i=" << i << " x=" << p[3*res+i].x << " y=" << p[3*res+i].y;
		}
	}
	if ( a <= b ) {
		for ( i=1; i<=res; i++ ) {
			p[res+i].x = p[res-i].x;
			p[res+i].y = -p[res-i].y;
		}
		for ( i=1; i<=res; i++ ) {
			p[res*2+i].x = -p[2*res-i].x;
			p[res*2+i].y = p[2*res-i].y;
		}
		for ( i=1; i<res; i++ ) {
			p[res*3+i].x = p[3*res-i].x;
			p[res*3+i].y = -p[3*res-i].y;
		}
	}
	for ( i=0; i<4*res; i++ )
		s.append( QString("%1,%2,").arg( qRound(p[i].x+xcenter) ).arg( qRound(p[i].y+ycenter) ) );
	s.chop(1);
	return s;
}

/** read a map using key=values stringlist */
bool Map::readFromHtml( char t, QString args ) {
	QStringList coords;
	qDebug() << "readFromHtml: type: " << t << " args: " << args;
	int i, x, y, radius, pos = 0;
	QRegExp nodes("[^a-z]*([a-z]+)=\"([^\"]*)\"");
	while ((pos = nodes.indexIn(args, pos)) != -1) {
		if ( nodes.cap(1).toLower()=="color" ) {
			color = nodes.cap(2);
		} else {
			qDebug() << "cap1=" << nodes.cap(1) << " cap2=" << nodes.cap(2);
			if ( nodes.cap(1).toLower()=="coords" ) {
				coords = nodes.cap(2).split(",");
				qDebug() << "coords: " << coords;
				if ( t=='C' ) {
					x = coords.at(0).simplified().toInt();
					y = coords.at(1).simplified().toInt();
					radius = coords.at(2).simplified().toInt();
					_points.append( new Point( this, QPoint(x,y) ) );
					_points.append( new Point( this, QPoint(x+radius,y) ) );
				} else { // R or P
					for ( i=0; i<coords.count(); i+=2 )
						if (i+1<coords.count()) {
							qDebug() << "x=" << coords.at(i).simplified() << " y=" << coords.at(i+1).simplified();
							x = coords.at(i).simplified().toInt();
							y = coords.at(i+1).simplified().toInt();
							_points.append( new Point( this, QPoint(x,y) ) );
						}
				}
			} else {
				for ( i=0; i<nblabels; i++ )
					if ( htmllabel[i].toLower()==nodes.cap(1).toLower() )
						htmlfields[i] = nodes.cap(2).replace( QString("&quot;"), QString("\"") );
			}
		}
		pos += nodes.matchedLength();
	}
	return true;
}

/** translate map to HTML */
QString Map::convert2Html () {
	QString shape, coords, attrs;
	if ( _points.count()<2 )
		return QString("");
	int x1 = _points.at(0)->xy.x();
	int y1 = _points.at(0)->xy.y();
	int x2 = _points.at(1)->xy.x();
	int y2 = _points.at(1)->xy.y();
	int radius;
	switch (type) {
		case 'R':
			shape = "rect";
			qDebug() << "R : x1=" << x1 << " y1=" << y1 << " x2=" << x2 << " y2=" << y2;
			coords = QString("%1,%2,%3,%4").arg(x1).arg(y1).arg(x2).arg(y2);
			break;
		case 'C':
			shape = "circle";
			radius = qMax( abs(x2-x1), abs(y2-y1) );
			qDebug() << "C: x1: " << x1 << " y1: " << y1 << "radius: " << radius;
			coords = QString("%1,%2,%3").arg(x1).arg(y1).arg(radius);
			break;
		case 'E': // ellipse (rectangle)
			shape = "poly";
			qDebug() << "E (rectangle): x1=" << x1 << " y1=" << y1 << " x2=" << x2 << " y2=" << y2;
			coords = ellipse2polygon( (double)(x1+x2)/2., (double)(y1+y2)/2., (double)(x2-x1)/2., (double)(y2-y1)/2. );
			break;
		case 'L': // ellipse (center/radius)
			shape = "poly";
			qDebug() << "L (center/radius): x1=" << x1 << " y1=" << y1 << " x2=" << x2 << " y2=" << y2;
			coords = ellipse2polygon( (double)x1, (double)y1, (double)(x2-x1), (double)(y2-y1) );
			break;
		case 'P':
			shape = "poly";
			for ( int i=0;i<_points.count(); i++ )
				coords.append( QString("%1,%2,").arg(_points[i]->xy.x()).arg(_points[i]->xy.y()) );
			coords.chop(1);
			qDebug() << "P: coord=" << coords;
			break;
	}
	// remember : color attribute don't exists in HTML !
	// we just save it as extra parameter for import to kmap again !
	QString s = QString("<area shape=\"%1\" coords=\"%2\" color=\"%3\" %4 />").arg(shape).arg(coords).arg( color.name() ).arg( getAttributes2String() );
	return s;
}

/** Return html options to hash */
QHash<QString, QString> Map::getAttributes2Hash () {
	QHash<QString, QString> hash;
	for ( int i=0; i<nblabels; i++ )
		if ( htmlfields.at(i).length() )
			hash[ htmllabel[i].toLower() ] = htmlfields.at(i);
	return hash;
}

/** Return html options to string */
QString Map::getAttributes2String () {
	QStringList s;
	for ( int i=0; i<nblabels; i++ )
		if ( htmlfields.at(i).length() ) {
			QString value = htmlfields.at(i);
			value.replace( QString("\""), QString("&quot;") );
			s.append( htmllabel[i].toLower() + "=\"" +  value + "\"" );
		}
	return s.join(" ");
}

formfieldset.h

Contrairement aux classes Point et Map, cette classe ne contient aucune donnée à enregistrer : elle sert uniquement à gérer l'affichage des widgets Qt (labels et champs texte), relatifs aux attributs HTML de la carte.

#ifndef FORMFIELDSET_H
#define FORMFIELDSET_H

#include <qlabel.h>
#include <qlineedit.h>

class FormFieldSet {
public:
	FormFieldSet( QString label, int isvisible );
	~FormFieldSet();
	/** toggle fieldset */
	void toggle();
	/** show or hide from visible */
	void display();
	/** show */
	void show();
	/** hide */
	void hide();
	QLabel * qlabel;
	QLineEdit * qlineedit;
	bool visible;
};

#endif

formfieldset.cpp

Comme dit précédemment, la classe FormFieldSet contrôle la visibilité des champs HTML affichés, via le menu principal principal de l'application et la classe KMap parente.

#include "formfieldset.h"

FormFieldSet::FormFieldSet( QString label, int isvisible ) {
	// create new label and linedit fields
	qlabel = new QLabel ( label );
	qlineedit = new QLineEdit;
	qlineedit -> setMaximumHeight( 40 );
	visible = ( isvisible ? true : false );
	display();
}

FormFieldSet::~FormFieldSet() {
}

/** toggle fieldset */
void FormFieldSet::toggle() {
	visible = !visible;
	display();
}

/** show or hide from visible */
void FormFieldSet::display() {
	// show or hide field
	visible ? show() : hide();
}

/** show */
void FormFieldSet::show() {
	visible = true;
	qlabel->show();
	qlineedit->show();
}

/** hide */
void FormFieldSet::hide() {
	visible = false;
	qlabel->hide();
	qlineedit->hide();
}

main.cpp

Avant d'étudier les classes Kmap et Canvas qui constituent le coeur du programme, nous changeons donc de côté d'étude comme annoncé plus haut.

Le fichier main.ccp est le point d'entrée classique d'un programme C++ sous Qt. Il définit une nouvelle QApplication, et initialise l'objet principal MainWindow qui, comme son nom l'indique, est la fenêtre principale visible par l'usager.

#include "mainwindow.h"

int main(int argc, char *argv[]) {
	QApplication a(argc, argv);
	MainWindow m;
	m.show();
	return a.exec();
}

mainwindow.h

A noter ici la présence du flag Qt3Support qui permet de supporter les anciens objets de Qt3, modifiés ou disparus dans Qt4.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <Qt3Support>

#include "kmap.h"
class Kmap;

#include "canvas.h"
class Canvas;

#include "formfieldset.h"
class FormFieldSet;

/** Kmap is the base class of the project */
class MainWindow : public QMainWindow
{
	Q_OBJECT
public:
	/** construtor */
	MainWindow( QWidget* parent=0 );
	/** destructor */
	~MainWindow();
	/** copy html exported maps to clipboard */
	QString kmapfile; // current KMAP filename
	QString htmlfile; // current HTML filename
	QString pictfile; // current picture
	void copyClipboard ( QString filename );
private:
	Kmap * _kmap;
	QLabel * statusbar1;
	QLabel * statusbar2;
	QProcess * process; // use for external commmunication with perl wrappers
signals:
	/** load a picture to canvas */
	void signalLoadPict( QPixmap pict );
public slots:
	/** for popup messages */
	void popupInformation( QString title, QString message );
	/** help */
	void slotHelp( bool b );
	/** shortcuts help */
	void slotShortcuts( bool b );
	/** status of kmap main application */
	void slotStatus1 ( const char * message );
	/** status of childs widgets */
	void slotStatus2 ( const char * message );
	/** load an external picture with dialog box */
	void slotLoadPict ();
	/** load an external picture with direct filename */
	void slotLoadPict ( QString filename );
	/** load a new map from a KMAP format */
	void slotLoadKmap ();
	/** save current map to KMAP format */
	void slotSaveKmap ();
	/** save current work under filename */
	void slotSaveAsKmap ();
	/** import map from html file */
	void slotImportHtml ();
	/** export current map to html format */
	void slotExportHtml ();
	/** export current map to xml format */
	void slotExportXml ();
};

#endif

mainwindow.cpp

La construction de notre fenêtre principale commence ici.

On initialise d'abord quelques icônes pour les différents boutons, puis on créé :

  • la barre du menu principal et les différents sous-menus
  • l'objet principal _kmap qui sera le MainWidget (la partie centrale) de notre application
  • deux zones de status en bas de la fenêtre

La classe définit également un bon nombre de signaux/slots, qui sont un peu l'équivalent des fonctions onchange, onmouseover, onfocus, ... que l'on retrouve couramment en programmation HTML.

L'idée est ici que chaque widget graphique peut émettre des signaux (via la méthode emit de Qt) pour prévenir les autres widgets d'un changement (l'utilisateur a pressé sur un bouton par exemple). Mais chaque widget peut aussi recevoir les signaux provenant des autres widgets, via des slots, qui sont de simples fonctions jouant le rôle de gestionnaires d'évènements. Avec ce mécanisme, faire une «connection» entre deux widgets revient finalement à lier un signal du widget source, vers un slot du widget cible, via la méthode connect de Qt.

Si la logique de Qt semble plutôt triviale, l'implémentation des signaux/slots peut se révéler parfois plus délicate dans la pratique... En outre, les boutons, champs de texte, listes, menus, etc. ont chacun leurs signaux propres, que l'on retrouve heureusement rapidement via l'aide de QtCreator. Enfin, l'utilisateur peut définir ses propres signaux dans une application, ce qui peut toujours être intéressant à exploiter. A l'usage, cette logique apparaît quand même bien pensée et cohérente pour paralléliser les traitements et scinder graphiquement les widgets entre eux.

Pour en revenir à notre application, quand l'utilisateur clique dans un menu, il déclenche un signal. On remarquera ici que un menu donné, les signaux sont envoyés à une classe intermédiaire, nommée QSignalMapper, qui permet de «concentrer» plusieurs signaux sur un même slot, local (dans la même classe) ou distant (classe parente ou descendante).

On remarquera enfin que la création des menus, via QAction, permet de facilement lier une icône et un raccourci clavier à chaque choix.

#include "mainwindow.h"

MainWindow::MainWindow( QWidget *parent ): QMainWindow( parent ) {

	setBackgroundRole(QPalette::Base);

	QPixmap fileopenkmap = QPixmap("16x16/actions/fileopen.png");
	QPixmap fileopenpict = QPixmap("16x16/filesystems/folder_yellow.png");
	QPixmap fileopenhtml = QPixmap("16x16/filesystems/folder_green.png");
	QPixmap filesave = QPixmap("16x16/actions/filesave.png");
	QPixmap filesavehtml = QPixmap("16x16/filesystems/folder_red.png");
	QPixmap fileprint = QPixmap("16x16/actions/fileprint.png");
	QPixmap filequit = QPixmap("16x16/actions/exit.png");
	QPixmap viewmagplus = QPixmap("16x16/actions/viewmag+.png");
	QPixmap viewmag = QPixmap("16x16/actions/viewmag+.png");
	QPixmap viewmagminus = QPixmap("16x16/actions/viewmag-.png");
	QPixmap editcopy = QPixmap("16x16/actions/editcopy.png");
	QPixmap editdel = QPixmap("16x16/actions/editdelete.png");
	QPixmap edit = QPixmap("16x16/actions/list.png");
	QPixmap tux = QPixmap("16x16/apps/devel/tux.png");
	QPixmap wizard = QPixmap("16x16/actions/wizard.png");

	kmapfile = "";
	htmlfile = "";
	pictfile = "";

	_kmap = new Kmap;
	setCentralWidget(_kmap);

	connect( _kmap->_canvas, SIGNAL( signalStatus1(const char *) ), this, SLOT( slotStatus1(const char *) ) );
	connect( _kmap->_canvas, SIGNAL( signalStatus2(const char *) ), this, SLOT( slotStatus2(const char *) ) );
	connect( _kmap, SIGNAL( signalStatus1(const char *) ), this, SLOT( slotStatus1(const char *) ) );
	connect( _kmap, SIGNAL( signalStatus2(const char *) ), this, SLOT( slotStatus2(const char *) ) );

	// MAIN MENU	FILE

	QMenu *menuFile = menuBar()->addMenu(tr("&File"));
	menuFile->addAction( QIcon(fileopenkmap), "&Open kmap", this, SLOT(slotLoadKmap()), Qt::CTRL+Qt::Key_O );
	menuFile->addAction( QIcon(filesave), "&Save", this, SLOT(slotSaveKmap()), Qt::CTRL+Qt::Key_S );
	menuFile->addAction( "&Save as...", this, SLOT(slotSaveAsKmap()) );
	menuFile->addSeparator();
	menuFile->addAction( QIcon(fileopenpict), "Open p&icture", this, SLOT(slotLoadPict()), Qt::CTRL+Qt::Key_I );
	menuFile->addSeparator();
	menuFile->addAction( "Import html", this, SLOT(slotImportHtml()) );
	menuFile->addAction( "Export html", this, SLOT(slotExportHtml()) );
	menuFile->addAction( "Export xml", this, SLOT(slotExportXml()) );
	menuFile->addSeparator();
	menuFile->addAction( QIcon(fileprint), "&Print", _kmap, SLOT(slotFilePrint()) );
	menuFile->addSeparator();
	menuFile->addAction( QIcon(filequit), "E&xit", qApp, SLOT(quit()), Qt::CTRL+Qt::Key_Q );

	// MAIN MENU MAP TYPE

	QMenu *menuMapType = menuBar()->addMenu(tr("&Type"));
	QSignalMapper * signalMapper1 = new QSignalMapper(this);

	QAction * actionMapType1 = new QAction( "&rectangle", this );
	actionMapType1->setShortcut( Qt::Key_R );
	signalMapper1->setMapping( actionMapType1, 1 );
	connect( actionMapType1, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	menuMapType->addAction( actionMapType1 );

	QAction * actionMapType2 = new QAction( "&circle (center/radius)", this );
	actionMapType2->setShortcut( Qt::Key_C );
	signalMapper1->setMapping( actionMapType2, 2 );
	connect( actionMapType2, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	menuMapType->addAction( actionMapType2 );

	// TODO: CIRCLE RECTANGLE

	QAction * actionMapType4 = new QAction( "&ellipse (rectangle)", this );
	actionMapType4->setShortcut( Qt::Key_E );
	signalMapper1->setMapping( actionMapType4, 4 );
	connect( actionMapType4, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	menuMapType->addAction( actionMapType4 );

	QAction * actionMapType5 = new QAction( "e&llipse (center/radius)", this );
	actionMapType5->setShortcut( Qt::Key_L );
	signalMapper1->setMapping( actionMapType5, 5 );
	connect( actionMapType5, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	menuMapType->addAction( actionMapType5 );

	QAction * actionMapType6 = new QAction( "&polygon", this );
	actionMapType6->setShortcut( Qt::Key_P );
	signalMapper1->setMapping( actionMapType6, 6 );
	connect( actionMapType6, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	menuMapType->addAction( actionMapType6 );

	connect( signalMapper1, SIGNAL(mapped(int)), _kmap->_canvas, SLOT(slotMapType(int)) );

	// MAIN MENU MAPS ACTIONS

	QMenu *menuMapAction = menuBar()->addMenu(tr("&Maps"));
	QSignalMapper * signalMapper2 = new QSignalMapper(this);

	QAction * actionMapAction1 = new QAction( QIcon(editcopy), "&Copy", this );
	actionMapAction1->setShortcut( Qt::CTRL+Qt::Key_C );
	signalMapper2->setMapping( actionMapAction1, 3 );
	connect( actionMapAction1, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction1 );

	QAction * actionMapAction2 = new QAction( QIcon(editdel), "&Delete", this );
	actionMapAction2->setShortcut( Qt::Key_D );
	signalMapper2->setMapping( actionMapAction2, 2 );
	connect( actionMapAction2, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction2 );

	menuMapAction->addSeparator();

	QAction * actionMapAction3 = new QAction( QIcon(edit), "Ed&it", this );
	actionMapAction3->setShortcut( Qt::Key_I );
	signalMapper2->setMapping( actionMapAction3, 8 );
	connect( actionMapAction3, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction3 );

	menuMapAction->addSeparator();

	QAction * actionMapAction4 = new QAction( "Select &All", this );
	actionMapAction4->setShortcut( Qt::CTRL+Qt::Key_A );
	signalMapper2->setMapping( actionMapAction4, 1 );
	connect( actionMapAction4, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction4 );

	menuMapAction->addSeparator();

	QAction * actionMapAction5 = new QAction( "Select &first map", this );
	actionMapAction5->setShortcut( Qt::CTRL+Qt::Key_F );
	signalMapper2->setMapping( actionMapAction5, 4 );
	connect( actionMapAction5, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction5 );

	QAction * actionMapAction6 = new QAction( "Select &previous map", this );
	actionMapAction6->setShortcut( Qt::CTRL+Qt::Key_P );
	signalMapper2->setMapping( actionMapAction6, 5 );
	connect( actionMapAction6, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction6 );

	QAction * actionMapAction7 = new QAction( "Select &next map", this );
	actionMapAction7->setShortcut( Qt::CTRL+Qt::Key_N );
	signalMapper2->setMapping( actionMapAction7, 6 );
	connect( actionMapAction7, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction7 );

	QAction * actionMapAction8 = new QAction( "Select &last map", this );
	actionMapAction8->setShortcut( Qt::CTRL+Qt::Key_L );
	signalMapper2->setMapping( actionMapAction8, 7 );
	connect( actionMapAction8, SIGNAL(triggered()), signalMapper2, SLOT(map()) );
	menuMapAction->addAction( actionMapAction8 );

	connect(signalMapper2, SIGNAL(mapped(int)), _kmap->_canvas, SLOT(slotMapAction(int)));

	// MAIN MENU POINTS

	QMenu *menuPoints = menuBar()->addMenu(tr("&Points"));
	menuPoints->addAction( QIcon(wizard), "Show/&hide control points", _kmap->_canvas, SLOT( slotControlPoints(bool) ), Qt::Key_H );

	// MAIN MENU FIELDS

	QMenu *menuFields = menuBar()->addMenu(tr("F&ields"));
	QSignalMapper * signalMapper4 = new QSignalMapper(this);

	for ( int i=0; i<nblabels; i++ ) {
		QString s1( htmllabel[i] );
		QAction * actionFields = new QAction( s1, this );
		signalMapper4->setMapping( actionFields, i );
		connect( actionFields, SIGNAL(triggered()), signalMapper4, SLOT(map()) );
		menuFields->addAction( actionFields );
	}

	connect(signalMapper4, SIGNAL(mapped(int)), _kmap, SLOT(slotToggleFormFieldSet(int)));

	// MAIN MENU ZOOM

	QMenu *menuZoom = menuBar()->addMenu(tr("&Zoom"));
	QSignalMapper * signalMapper5 = new QSignalMapper(this);

	QAction * actionZoom1 = new QAction( QIcon(viewmagminus), "&50%", this );
	signalMapper5->setMapping( actionZoom1, 50 );
	connect( actionZoom1, SIGNAL(triggered()), signalMapper5, SLOT(map()) );
	menuZoom->addAction( actionZoom1 );

	QAction * actionZoom2 = new QAction( QIcon(viewmag), "&100%", this );
	signalMapper5->setMapping( actionZoom2, 100 );
	connect( actionZoom2, SIGNAL(triggered()), signalMapper5, SLOT(map()) );
	menuZoom->addAction( actionZoom2 );

	QAction * actionZoom3 = new QAction( QIcon(viewmagplus), "&200%", this );
	signalMapper5->setMapping( actionZoom3, 200 );
	connect( actionZoom3, SIGNAL(triggered()), signalMapper5, SLOT(map()) );
	menuZoom->addAction( actionZoom3 );

	connect(signalMapper5, SIGNAL(mapped(int)), _kmap, SLOT(slotZoom(int)));

	// MAIN MENU HELP

	QMenu *menuhelp = menuBar()->addMenu(tr("&Help"));
	menuhelp->addAction( QIcon(tux), "A&bout", this, SLOT( slotHelp(bool) ), Qt::Key_B );
	menuhelp->addAction( "Shortcuts", this, SLOT( slotShortcuts(bool) ) );

	// STATUS BAR

	statusbar1 = new QLabel;
	statusbar1 -> setFrameStyle( QFrame::Panel | QFrame::Sunken );
	statusbar1 -> setMaximumHeight( 20 );
	statusBar()->addPermanentWidget( statusbar1, 1 );

	statusbar2 = new QLabel;
	statusbar2 -> setFrameStyle( QFrame::Panel | QFrame::Sunken );
	statusbar2 -> setMaximumHeight( 20 );
	statusBar()->addPermanentWidget( statusbar2, 1 );

	/*
		// TODO !
		QToolBar *toolbar1 = addToolBar(tr("File"));
		toolbar1->addAction( QIcon( fileopenhtml ), "Open HTML", this, SLOT(slotLoadKmap()) );
		toolbar1->addAction( QIcon( filesave ), "Save Maps", this, SLOT(slotSaveKmap()) );
		toolbar1->addAction( QIcon( fileprint ), "Print", this, SLOT(slotFilePrint()) );
		toolbar1->addAction( QIcon( filequit ), "Quit", qApp, SLOT(quit()) );

	*/

	connect( this, SIGNAL( signalLoadPict(QPixmap) ), _kmap->_canvas, SLOT( slotLoadPict(QPixmap) ) );
	connect( _kmap->_canvas, SIGNAL( signalLoadPict(QString) ), this, SLOT( slotLoadPict(QString) ) );
}

MainWindow::~MainWindow()
{
}

/** for popup messages */
void MainWindow::popupInformation( QString title, QString message ) {
	QMessageBox mb( title,
	message,
	QMessageBox::Information,
	QMessageBox::NoButton,
	QMessageBox::No |QMessageBox::Escape,
	QMessageBox::NoButton
	);
	mb.exec();
}

/** help */
void MainWindow::slotHelp( bool b ) {
	popupInformation( tr("About"),
	"<h4>KMAP - a KDE Imagemap Editor - v2.0</h4>"
	"<p>(c) 2012 - Jean Luc Biellmann</p>"
	"<p>contact@alsatux.com</p>"
	);
}

/** shortcuts help */
void MainWindow::slotShortcuts( bool b ) {
	popupInformation( tr("Shortcuts"),
	"<p>On drawing:</p>"
	"<ul><li>H: show/hide control points</li>"
	"<li>R: select rectangle tool</li>"
	"<li>C: select circle tool</li>"
	"<li>E: select ellipse/rectangle tool</li>"
	"<li>L: select ellipse/center-radius tool</li>"
	"<li>P: select polygon tool</li></ul>"
	"<p>On a polygon control point:</p>"
	"<ul><li>A: add two points</li>"
	"<li>D: delete the point</li></ul>"
	"<p>On selected map(s) (group operations):</p>"
	"<ul><li>CTRL+C: copy</li>"
	"<li>D: delete</li>"
	"<li>I: edit</li></ul>"
	"<p>On the canvas:</p>"
	"<ul><li>arrows: move to top, right, bottom or left</li>"
	"<li>page up/down: move to top or bottom</li></ul>"
	"<p>ESC : abort current operation</p>"
	);
}

/** status of kmap main application */
void MainWindow::slotStatus1 ( const char * message ) {
	QString s(message);
	statusbar1->clear();
	statusbar1->setText( s );
}

/** status of childs widgets */
void MainWindow::slotStatus2 ( const char * message ) {
	QString s(message);
	statusbar2->clear();
	statusbar2->setText( s );
}

/** load an external picture with dialog box */
void MainWindow::slotLoadPict () {
	QString filename = QFileDialog::getOpenFileName( this, tr("Load a picture"), kmapfile, tr("Pictures")+" (*.png *.xpm *.jpg)" );
	slotLoadPict( filename );
}

/** load an external picture with direct filename */
void MainWindow::slotLoadPict ( QString filename ) {
	QPixmap pict;
	if ( !filename.isEmpty() ) {
		if ( !pict.load(filename) ) {
			QMessageBox::warning( 0, tr("Error"), tr("Cannot load the file") );
		} else {
			pictfile = filename;
			emit signalLoadPict( pict );
		}
		slotStatus1( tr("Picture loaded") );
		slotStatus2( QString( tr("Picture") + ": %1x%2" ).arg(pict.width()).arg(pict.height()) );
	} else {
		slotStatus1( tr("No file to load ?") );
		slotStatus2( "" );
	}
}

/** load a new map from a KMAP file */
void MainWindow::slotLoadKmap () {
	QString filename = QFileDialog::getOpenFileName( this, tr("Load kmap file"), kmapfile, "Kmap (*.kmap)" );
	if ( !filename.isEmpty() ) {
		kmapfile = filename;
		slotStatus1( tr("Reading current maps") );
		if ( _kmap->_canvas->loadKmapFile( kmapfile ) )
			slotStatus1( tr("Read -> Ok") );
		else
			slotStatus1( tr("Read -> Failed") );
	}
}

/** save current map to KMAP format */
void MainWindow::slotSaveKmap () {
	if ( kmapfile.isEmpty() ) {
		slotSaveAsKmap();
	} else {
		slotStatus1( tr("Saving current maps") );
		_kmap->_canvas->saveMaps2Kmap( kmapfile, pictfile );
		slotStatus1( tr("Save -> Ok") );
	}
}

/** save current work under filename */
void MainWindow::slotSaveAsKmap () {
	qDebug() << "slotSaveAsKmap: ";
	QString filename = QFileDialog::getSaveFileName( this, tr("Save as"), kmapfile, "Kmap (*.kmap)" );
	if ( !filename.isEmpty() ) {
		if ( !filename.endsWith(".kmap") )
			filename += ".kmap";
		kmapfile = filename;
		slotStatus1( tr("Saving current maps") );
		_kmap->_canvas->saveMaps2Kmap( kmapfile, pictfile );
		slotStatus1( tr("Save -> Ok") );
	}
}

/** copy html exported maps to clipboard */
void MainWindow::copyClipboard ( QString filename ) {
	QFile f( filename );
	if ( !f.open( QIODevice::ReadOnly ) )
		return;
	QClipboard *clipboard = QApplication::clipboard();
	QTextStream ts( &f );
	clipboard->setText( ts.readAll() );
	f.close();
}

// IMPORT AND EXPORT

/** import map from html file */
void MainWindow::slotImportHtml () {
	qDebug() << "slotImportHtml: ";
	QString htmlfilename = QFileDialog::getOpenFileName( this, tr("Import HTML file"), "", "Html (*html)" );
	if ( !htmlfilename.isEmpty() ) {
		if ( !htmlfilename.endsWith( ".html" ) )
				htmlfilename += ".html";
		htmlfile = htmlfilename;
		if ( _kmap->_canvas->loadHtmlFile( htmlfile ) ) {
			slotStatus1( tr("Reading maps") );
		} else {
			slotStatus1( tr("Import -> Failed") );
		}
	}
}

/** export current map to html format */
void MainWindow::slotExportHtml () {
	qDebug() << "slotExportHtml: ";
	QString htmlfilename = QFileDialog::getSaveFileName( this, tr("Export to HTML"), "", "Html (*html)" );
	if ( !htmlfilename.isEmpty() ) {
		if ( !htmlfilename.endsWith( ".html" ) )
				htmlfilename += ".html";
		htmlfile = htmlfilename;
		slotStatus1( tr("Saving maps") );
		if ( _kmap->_canvas->saveMaps2Html( htmlfile, pictfile ) )
			slotStatus1( tr("Export -> Ok") );
		else
			slotStatus1( tr("Export -> Failed") );
	}
}

/** export current map to xml format */
void MainWindow::slotExportXml () {
	QString xmlfilename = QFileDialog::getSaveFileName( this, tr("Export to XML"), "", "Xml (*.xml)" );
	if ( !xmlfilename.isEmpty() ) {
		if ( !xmlfilename.endsWith( ".xml" ) )
				xmlfilename += ".xml";
		if ( _kmap->_canvas->saveMaps2Xml( xmlfilename, pictfile ) )
			slotStatus1( tr("Save Xml -> Ok") );
		else
			slotStatus1( tr("Save Xml -> Failed") );
	}
}

kmap.h

Pour faire court (on rentrera plus loin dans les détails), ce fichier définit entre autres les couleurs de Qt qui nous servirons de couleur de tracé pour les cartes.

J'ai été ici obligé de conserver la compatibilité Qt3 pour l'objet Q3ScrollView, dont je n'ai malheureusement pas trouvé un équivalent satisfaisant en Qt4. Si quelqu'un a un tuyau sur ce point, je suis évidemment preneur !

#ifndef KMAP_H
#define KMAP_H

//#ifdef HAVE_CONFIG_H
//#include <config.h>
//#endif

#include "mainwindow.h"
class MainWindow;

#include "canvas.h"
class Canvas;

#include "formfieldset.h"
class FormFieldSet;

// default qt colors values and names
const QColor _qtcolor[17] = {
    QColor( Qt::black ),
    QColor( Qt::darkGray ),
    QColor( Qt::gray ),
    QColor( Qt::lightGray ),
    QColor( Qt::white ),
    QColor( Qt::red ),
    QColor( Qt::green),
    QColor( Qt::blue ),
    QColor( Qt::cyan ),
    QColor( Qt::magenta ),
    QColor( Qt::yellow ),
    QColor( Qt::darkRed ),
    QColor( Qt::darkGreen ),
    QColor( Qt::darkBlue),
    QColor( Qt::darkCyan ),
    QColor( Qt::darkMagenta ),
    QColor( Qt::darkYellow ),
};

/** Kmap is the base class of the project */
class Kmap : public QWidget
{
	Q_OBJECT
public:
	/** construtor */
	Kmap( QWidget* parent=0 );
	/** destructor */
	~Kmap();
	Canvas * _canvas;
	Q3ScrollView * qscrollview;
private:
	// formular fields
	QList<FormFieldSet *> _formfieldset;
public slots:
	/** print current picture with mapped parts */
	void slotFilePrint ();
	/** change current draw color */
	void slotChangeDrawSelectionColor( int color );
	/** change current map color */
	void slotChangeMapCurrentColor( int color );
	/** change HTML entries */
	void slotChangeText ( QStringList _htmlfields );
	/** handle scrolling events from canvas */
	void slotMouseScrolling( QPoint offset );
	/** scroll to new position */
	void slotScrollBy( QPoint pos );
	/** show/hide HTML fields */
	void slotToggleFormFieldSet( int i );
	/** show HTML fields */
	void slotShowFormFieldSet( int i );
	/** change current data for all selected maps */
	void slotTextChanged ( const QString & );
	/** update qscrollview according to current edited object */
	void slotCenterView ( double x, double y );
	/** give focus to HREF */
	void slotFocusText ();
	/** set zoom factor */
	void slotZoom ( int zoom );
	/** handle events of menu Maps */
	void slotMaps ( int i );

signals: // Signals
	/** send text to status 1 */
	void signalStatus1( const char * text );
	/** send text to status 2 */
	void signalStatus2( const char * text );
};

#endif

kmap.cpp

Situé entre la fenêtre principale et le canevas de dessin, la classe Kmap est le widget principal de notre application, et définie trois zones :

  • le canevas de dessin principal, dans lequel on pourra importer une image externe
  • les couleurs de tracé sous forme de petits carrés de couleur
  • les attributs HTML, via un tableau de labels/champs textes

Les attributs peuvent être activés/désactivés via le menu de l'application, pour ne conserver que les options utiles à l'écran.

Par défaut sous Qt, un canevas de dessin n'a pas d'ascenseurs pour le défilement. C'est la raison pour laquelle il est encapsulé dans une Q3ScrollView.

Pour le reste, la classe Kmap est plus une classe de contrôle, pour réagir aux différents signaux, provenant soit du widget parent (MainWindow), soit des widgets enfants (et notamment Canvas).

#include "kmap.h"

// list of HTML entities with default visible value
QString htmllabel[] = {"HREF", "ALT", "ACCESSKEY", "CLASS", "ID", "LANG", "ONBLUR", "ONCLICK", "ONDBLCLICK", "ONFOCUS", "ONKEYDOWN", "ONKEYPRESS", "ONKEYUP", "ONMOUSEDOWN", "ONMOUSEMOVE", "ONMOUSEOUT", "ONMOUSEOVER", "ONMOUSEUP", "STYLE", "TABINDEX", "TABORDER", "TARGET", "TITLE" };
int showlabel[] = {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
int nblabels = 23;

Kmap::Kmap( QWidget *parent ): QWidget(parent) {

	// layout the layout from top to bottom
	QVBoxLayout * qvboxlayout1 = new QVBoxLayout( this );

	// qscrollview + CANVAS
	qscrollview = new Q3ScrollView;
	_canvas = new Canvas( this );
	_canvas->setCursor( Qt::CrossCursor );
	qscrollview->addChild( _canvas, 0, 0 );
	qvboxlayout1->addWidget( qscrollview, 10 );

	QBoxLayout * qboxlayout1 = new QHBoxLayout;
	qvboxlayout1->addLayout( qboxlayout1, 1 );

	// QButtonGroup make no visual change - just group some properties
	QButtonGroup * qbuttongroup1 = new QButtonGroup;

	QPixmap square( 20, 20 );
	for ( int i=0; i<16; i++ ) {
		QPushButton * qpushbutton1 = new QPushButton;
		qpushbutton1->setFixedSize( 22, 22 );
		square.fill( _qtcolor[i] );
		qpushbutton1->setIcon( square );
		QToolTip::add( qpushbutton1, "set map color" );
		qboxlayout1->addWidget( qpushbutton1, 1 );
		qbuttongroup1->addButton( qpushbutton1, i+1 );
	}

	qvboxlayout1->addLayout( qboxlayout1, 10 );
	connect( qbuttongroup1, SIGNAL( buttonClicked(int) ), this, SLOT( slotChangeMapCurrentColor(int) ) );

	// HTML FIELDSETS
	QGridLayout * qgridlayout1 = new QGridLayout;
	qvboxlayout1->addLayout( qgridlayout1, 1 );
	for ( int i=0; i<nblabels; i++ ) {
		// create new label and linedit fields
		_formfieldset.append( new FormFieldSet( htmllabel[i], showlabel[i] ) );
		qgridlayout1-> addWidget( _formfieldset.at(i)->qlabel, i, 0 );
		qgridlayout1-> addWidget( _formfieldset.at(i)->qlineedit, i, 1 );
		// connect to application
		connect( _formfieldset.at(i)->qlineedit, SIGNAL( textChanged(const QString &) ),
				this, SLOT(slotTextChanged(const QString &)) );
	}

	// SIGNALS / SLOTS
	connect( _canvas, SIGNAL( signalChangeText(QStringList) ), this, SLOT( slotChangeText(QStringList) ) );
	connect( _canvas, SIGNAL( signalFocusText() ), this, SLOT( slotFocusText() ) );
	connect( _canvas, SIGNAL( signalMouseScrolling(QPoint) ), this, SLOT( slotMouseScrolling(QPoint) ) );
	connect( _canvas, SIGNAL( signalScrollBy(QPoint) ), this, SLOT( slotScrollBy(QPoint) ) );
	connect( _canvas, SIGNAL( signalCenterView (double,double) ), this, SLOT( slotCenterView(double,double) ) );
}

Kmap::~Kmap()
{
}

/** print current picture with mapped parts */
void Kmap::slotFilePrint () {
	_canvas->printMaps();
}

/** change current map color */
void Kmap::slotChangeMapCurrentColor( int color ) {
	qDebug() << "slotChangeMapCurrentColor";
	_canvas->mapcurrentcolor = _qtcolor[ color-1 ];
	for ( int i=0; i<int( _canvas->maps.count() ); i++ )
		if ( _canvas->maps.at(i)->isgrabbed )
			_canvas->maps.at(i)->color = _canvas->mapcurrentcolor;
	QString s;
	s = QString("Color: %1").arg(_canvas->mapcurrentcolor.name());
	signalStatus1( s.toUtf8() );
}

/** change current draw color */
void Kmap::slotChangeDrawSelectionColor( int color ) {
	_canvas->drawselectioncolor = _qtcolor[ color ];
}

/** change HTML entries */
void Kmap::slotChangeText ( QStringList htmlfields ) {
	for (int i=0; i<nblabels; i++ ) {
		_formfieldset.at(i)->qlineedit->blockSignals( TRUE ); // disable signal textChanged()
		_formfieldset.at(i)->qlineedit->setText( htmlfields.at(i) );
		_formfieldset.at(i)->qlineedit->blockSignals( FALSE ); // enable signal textChanged()
	}
}

/** handle scrolling events from canvas */
void Kmap::slotMouseScrolling( QPoint offset ) {
	qscrollview->scrollBy( offset.x(), offset.y() );
}


/** scroll to new position */
void Kmap::slotScrollBy( QPoint pos ) {
	qscrollview -> scrollBy( pos.x(), pos.y() );
}

/** show/hide HTML fields */
void Kmap::slotToggleFormFieldSet( int i ) {
	qDebug() << "slotToggleFormFieldSet: " << i;
	_formfieldset.at(i)->toggle();
	repaint();
}

/** show/hide HTML fields */
void Kmap::slotShowFormFieldSet( int i ) {
	qDebug() << "slotShowFormFieldSet: " << i;
	_formfieldset.at(i)->show();
	repaint();
}

/** change current data for all selected maps */
void Kmap::slotTextChanged ( const QString &text ) {
	QWidget * qwidget = focusWidget();
	for ( int i=0; i<nblabels; i++ )
		if ( qwidget == _formfieldset.at(i)->qlineedit )
			_canvas->updHtmlFieldForSelectedMaps( i, text );
}

/** update qscrollview according to current edited object */
void Kmap::slotCenterView ( double x, double y ) {
	int i = int( x*_canvas->zoomfactor/100. );
	int j = int( y*_canvas->zoomfactor/100. );
	qscrollview->center( i, j );
	_canvas->repaint();
}

/** give focus to HREF */
void Kmap::slotFocusText () {
	_formfieldset[0]->qlineedit->setFocus();
}

/** set zoom factor */
void Kmap::slotZoom ( int zoom ) {
	qDebug() << "slotZoom: ";
	// record center of the qscrollview as percentage
	double x = ((double)qscrollview->contentsX() + (double)qscrollview->visibleWidth()/2)/(double)qscrollview->contentsWidth();
	double y = ((double)qscrollview->contentsY() + (double)qscrollview->visibleHeight()/2)/(double)qscrollview->contentsHeight();
	qDebug() << "x=" << x << "% y=" << y << "%";
	// update canvas
	_canvas->zoom( (double) zoom );
	// zoom will change contents size
	int w = _canvas->width();
	int h = _canvas->height();
	qscrollview->resizeContents( w, h );
	// new qscrollview position
	qscrollview->center( (int)(x*w), (int)(y*h) );
}

/** handle events of menu Maps */
void Kmap::slotMaps ( int i ) {
	switch( i ) {
		case 1: _canvas->select(); break;
		case 2: _canvas->del(); break;
		case 3: _canvas->copy(); break;
	}
}

canvas.h

Le canevas principal gère les tracés réels (cartes) et les lectures/écritures de fichier.

#ifndef CANVAS_H
#define CANVAS_H

#include <QWidget>
#include <QPaintEvent>
#include <QPrinter>
#include <QTextStream>
#include <QDebug>
#include <QPainter>
#include <QFile>
#include <QMenu>
#include <QAction>
#include <QSignalMapper>
#include <QXmlStreamWriter>
#include <QPaintDevice>

#include <math.h>

#include "map.h"
class Map;

#include "point.h"
class Point;

extern QString htmllabel[];
extern int nblabels;

class Canvas : public QWidget	{
	Q_OBJECT
public:
	Canvas( QWidget * parent );
	~Canvas();
	/** copy selected maps and translate result */
	void copy ();
	/** delete polygonal point or full maps according to context */
	void del ();
	/** only if one map is grabbed */
	void gotoMap ( int n );
	/** handle key events in canvas */
	void keyPressEvent ( QKeyEvent * event );
	/** read maps from a kmap file */
	bool loadKmapFile ( QString filename );
	/** read maps from HTML */
	bool loadHtmlFile ( QString htmlfile );
	/** display new fieldsets according to current maps */
	void reloadFormFieldSet ();
	/** save current map to KMAP */
	bool saveMaps2Kmap ( QString kmapfile, QString pictfile );
	/** save current map to HTML */
	bool saveMaps2Html ( QString kmapfile, QString pictfile );
	/** save current map to XML */
	bool saveMaps2Xml ( QString xmlfile, QString pictfile );
	/** select all maps */
	void select ();
	/** update text to _htmlfield i using selected maps */
	void updHtmlFieldForSelectedMaps ( int j, QString text );
	/** return a copy of background sized to current zoom factor */
	void zoom ( double newzoomfactor );
	/** show current map type in status2 */
	void showMapType ( char c );
	/** update the last segment of the current map */
	void drawSegment ();
	/** go to edit mode after a keypress event */
	void edit ();
	/** abort current selection */
	void abort ();
	/** print current maps to printer */
	void printMaps();
	double zoomfactor;
	QColor drawselectioncolor; // color for mouse selection
	QColor mapcurrentcolor;	// current map color
	QList<Map *> maps;			// double list pointers of Maps
	QPixmap bufferPicture; // buffer for picture layer
	QPixmap bufferMaps; // buffer for maps layer
	QPainter printerPainter; // printer buffer
private:
	/** add points to polygon */
	void add ();
	/** delete a map */
	void del ( Map * _map );
	/** delete a polygonal point */
	void del ( Point * _point );
	/** draw to screen and buffer if needed */
	void drawPoints ( Point * _p1, Point * _p2 );
	/** draw an object using points p1 and p2 to pixmap	*/
	void drawTo( QPainter * painter, Point * _p10, Point * _p20 );
	/** draw a single map */
	void drawMap ( Map * _map );
	/** used for moving a polygon point */
	void drawMapExcept( Point * _point );
	/** redraw all maps */
	void drawMaps ();
	/** return current map index */
	int findMap ( Map * _map );
	/** return current point index */
	int findMap ( Point * _point );
	/** return current point index */
	int findPoint ( Point * _point );
	/** grab all maps */
	void grab ();
	/** start to grab a map */
	void grab ( Map * _map );
	/** test if the given point is in an object */
	Map * isCursorInMap ( QPoint point );
	/** deselect all maps */
	void ungrab ();
	/** stop to grab a map */
	void ungrab ( Map * _map );
	/** search the nearest point using mouse position */
	Point * searchNearestPoint( QPoint p );
	bool isdrawing;			 // current drawing state
	bool ismoving;			 // current moving state
	bool isgrabbing;			 // current grabbing state
	bool isscrolling;			 // current scrolling state
	bool toprinter; // send to printer
	bool hidecontrolpoint; // hiding control points
	char mapcurrenttype;	// current map type
	Map * _currentmap;	// we need to store the current map while drawing
	Point * _currentpoint; // we need to store the current point while drawing
	QPoint lastmousepos; // last mouse position
	QSignalMapper * signalMapper1; // signal mapper for popup menu type
	QMenu * popupMenuMapType; // popup menu map type
protected:
	/** handle paint events in the canvas */
	virtual void paintEvent( QPaintEvent * event );
	/** handle mouse move events */
	virtual void mouseMoveEvent( QMouseEvent * event );
	/** handle mouse press events */
	virtual void mousePressEvent( QMouseEvent * event );
	/** handle mouse release buttons events */
	virtual void mouseReleaseEvent ( QMouseEvent * event );
	/** handle mouse wheel events */
	virtual void wheelEvent ( QWheelEvent * event );
public slots:
	/** load a background picture */
	void slotLoadPict ( QPixmap p );
	/** update current map type */
	void slotMapType ( int i );
	/** show or hide control points */
	void slotControlPoints ( bool b );
	/** handle actions on selected maps */
	void slotMapAction ( int i );
signals: // Signals
	/** update HTML entries */
	void signalChangeText( QStringList htmlfields );
	/** scroll the canvas with middle button mouse */
	void signalMouseScrolling( QPoint offset );
	/** load the background picture */
	void signalLoadPict( QString filename );
	/** send text to status 1 */
	void signalStatus1( const char * text );
	/** send text to status 2 */
	void signalStatus2( const char * text );
	/** send the new position to qscrollview */
	void signalScrollBy( QPoint pos );
	/** center the viewport according to real coordinates */
	void signalCenterView ( double x, double y );
	/** give focus to text element HREF */
	void signalFocusText();
	/** show formfieldset */
	void signalShowFormFieldSet( int i );
};

#endif

canvas.cpp

Cette dernière classe est la classe de dessin proprement dite. C'est évidemment la classe la plus longue et la plus complexe !

Pour le dessin, nous jouons ici sur deux buffers :

  • bufferPicture qui contient l'image de fond à importer
  • bufferMaps qui contient le tracé des cartes proprement dit

En superposant les deux buffers, nous obtenons le rendu voulu, à l'écran ou à l'impression. La librairie utilise la classe QPainter et la méthode repaint pour dessiner sur le canevas. Cette dernière appelle en fait la méthode paintEvent qui doit être implémentée dans le programme.

La gestion de la souris repose sur les méthodes mouseMoveEvent, mousePressEvent, mouseReleaseEvent et wheelEvent. On remarquera la similitude avec les gestionnaires d'événements classiques en HTML.

La gestion du clavier est laissée aux soins de la méthode keyPressEvent.

#include "canvas.h"

Canvas::Canvas( QWidget * parent ) {

	popupMenuMapType = new QMenu( this );
	signalMapper1 = new QSignalMapper( this );

	QAction * actionMapType1 = new QAction( "&rectangle", this );
	actionMapType1->setShortcut( Qt::Key_R );
	signalMapper1->setMapping( actionMapType1, 1 );
	connect( actionMapType1, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	popupMenuMapType->addAction( actionMapType1 );

	QAction * actionMapType2 = new QAction( "&circle (center/radius)", this );
	actionMapType2->setShortcut( Qt::Key_C );
	signalMapper1->setMapping( actionMapType2, 2 );
	connect( actionMapType2, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	popupMenuMapType->addAction( actionMapType2 );

	// TODO: CIRCLE RECTANGLE

	QAction * actionMapType4 = new QAction( "&ellipse (rectangle)", this );
	actionMapType4->setShortcut( Qt::Key_E );
	signalMapper1->setMapping( actionMapType4, 4 );
	connect( actionMapType4, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	popupMenuMapType->addAction( actionMapType4 );

	QAction * actionMapType5 = new QAction( "e&llipse (center/radius)", this );
	actionMapType5->setShortcut( Qt::Key_L );
	signalMapper1->setMapping( actionMapType5, 5 );
	connect( actionMapType5, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	popupMenuMapType->addAction( actionMapType5 );

	QAction * actionMapType6 = new QAction( "&polygon", this );
	actionMapType6->setShortcut( Qt::Key_P );
	signalMapper1->setMapping( actionMapType6, 6 );
	connect( actionMapType6, SIGNAL(triggered()), signalMapper1, SLOT(map()) );
	popupMenuMapType->addAction( actionMapType6 );

	connect( signalMapper1, SIGNAL(mapped(int)), this, SLOT(slotMapType(int)) );
	connect( this, SIGNAL(signalShowFormFieldSet(int)), parent, SLOT(slotShowFormFieldSet(int)) );

	// set default size for buffers
	bufferPicture = QPixmap( size() );
	bufferPicture.fill( Qt::transparent );
	bufferMaps = QPixmap( size() );
	bufferMaps.fill( Qt::transparent );

	// default trace color
	drawselectioncolor = "red";

	// default map parameters
	slotMapType(1); // mapcurrenttype = 'R' (rectangle)
	mapcurrentcolor = "blue";
	_currentmap = NULL;
	_currentpoint = NULL;

	// default drawing parameters
	isdrawing = false; // not currently drawing something
	ismoving = false; // not moving a polygon point (isdrawing required)
	isgrabbing = false; // no map grabbed
	isscrolling = false; // no mouse scrolling on canvas

	// show control points
	hidecontrolpoint = false;

	// do not send to printer
	toprinter = false;

	setMouseTracking( true ); // enable mouse tracking for mouseMoveEvent

	// default zoom factor is 100% with initialisation of backgoundz
	zoomfactor = 100.;
}

Canvas::~Canvas() {
}

/** load a background picture */
void Canvas::slotLoadPict ( QPixmap pict ) {
	// WE DON'T CHANGE CANVAS SIZE HERE !
	// we just adapt sizes of picture and maps buffer, then record picture
	QSize pictsize = pict.size();
	bufferPicture = QPixmap( pictsize );
	bufferMaps = QPixmap( pictsize );
	bufferPicture = pict.copy();
	//qDebug() << "slotLoadPict:";
	//qDebug() << "bufferPicture size: " << bufferPicture.size();
	//qDebug() << "bufferMaps size: " << bufferMaps.size();
	repaint();
}

/** draw an object using points p1 and p2 to pixmap	*/
void Canvas::drawTo( QPainter * painter, Point * _p10, Point * _p20 ) {
	Map * _map = _p10->_parentmap;

	if ( ( ismoving && _map->ismoved ) || ( isgrabbing && _map->isgrabbed ) )
		painter->setPen( drawselectioncolor );
	else
		painter->setPen( _p10->_parentmap->color );

	QPoint p1 = _p10->xy, p2 = _p20->xy , p3;

	if ( _map->type == 'R' ) {
		QRect r( p1, p2 );
		painter->drawRect( r );
	}

	if ( _map->type == 'C' ) {
	 p3 = p2-p1;
	 p3.setX( abs(p3.x()) );
	 p3.setY( abs(p3.y()) );
	 if ( p3.x() > p3.y() )
	 	p3.setY( p3.x() );
	 else
	 	p3.setX( p3.y() );
	 QRect r(p1-p3,p1+p3);
	 painter->drawEllipse( r );
	}

	if ( _map->type == 'E' ) {
	 QRect r(p1,p2);
	 painter->drawEllipse( r );
	}

	if ( _map->type == 'L' ) {
	 QRect r(p1-(p2-p1),p2);
	 painter->drawEllipse( r );
	}

	if ( _map->type == 'P' ) {
	 painter->drawLine( p1, p2 );
	}

	// draw a little circle around control points
	if ( !hidecontrolpoint ) {
		QPoint c ( 2, 2 );
		QRect r1(p1-c,p1+c);
		painter->drawEllipse(r1);
		QRect r2(p2-c,p2+c);
		painter->drawEllipse(r2);
	}
}

/** draw to screen and bufferDefault if needed */
void Canvas::drawPoints ( Point * _p1, Point * _p2 ) {
	QPainter painter;

	//qDebug() << "drawPoints:";
	//qDebug() << bufferMaps.size();
	painter.begin( &bufferMaps ); // always record to screen
	drawTo( &painter, _p1, _p2 );
	painter.end();

	/*
	if ( toprinter )
		drawTo( &printerPainter, _p1, _p2 );
*/
}

/** handle paint events in the canvas */
void Canvas::paintEvent( QPaintEvent * event ) {
	//qDebug() << "paintEvent:";

	resize( bufferPicture.size()*(zoomfactor/100.) );

	//qDebug() << "Canvas size:" << size();
	//qDebug() << "bufferPicture size:" << bufferPicture.size();
	//qDebug() << "bufferMaps size:" << bufferMaps.size();

	QPainter painter;
	painter.begin( this );

	QPixmap background( size() );
	background.fill( Qt::white );
	painter.drawPixmap( 0, 0, background );

	QTransform matrix( zoomfactor/100., 0., 0., zoomfactor/100., 0., 0. );
	painter.setTransform( matrix );

	painter.drawPixmap( 0, 0, bufferPicture );
	drawMaps();
	painter.drawPixmap( 0, 0, bufferMaps );

	painter.end();
}

/** handle mouse press events in canvas */
void Canvas::mousePressEvent( QMouseEvent * event ) {
	//qDebug() << "mousePressEvent: " << event;

	QPoint realpos = event->pos() * 100. / zoomfactor;
	Map * _map;
	int i;

	if ( !isscrolling ) { // priority to scrolling
		if ( isgrabbing ) { // currently grabbing
				if ( event->button() == Qt::LeftButton ) {
				_map = isCursorInMap( realpos );
				if ( event->modifiers().testFlag(Qt::ControlModifier) ) {
					if ( _map != NULL ) { // map found
						if ( !_map->isgrabbed ) { // add the new map to grabbing selection
							grab( _map );
							repaint();
						} else { // remove map from current selection
							ungrab( _map );
							repaint();
						}
					}
				}
				if ( event->modifiers().testFlag(Qt::NoModifier) ) {
					if ( _map == NULL ) { // no map found
						ungrab(); // deselect all
						repaint();
					}
				}
			}
			if ( event->button() == Qt::RightButton ) // deselect all
				ungrab();
				repaint();
		} else { // not currently grabbing object
			if ( event->button() == Qt::LeftButton ) {
				if ( isdrawing == false ) { // not currently drawing
					qDebug() << "realpos = " << realpos;
					_currentpoint = searchNearestPoint( realpos );
					qDebug() << _currentpoint;
					if ( _currentpoint == NULL ) { // no control point selected
						qDebug() << "_currentpoint is NULL (no ctrl point selected)";
						_map = isCursorInMap( realpos );
						if ( _map != NULL ) { // we are currently in an object
							grab( _map );
							repaint();
						} else { // not in an object : create a new one
							isdrawing = true; // start to draw
							qDebug() << "isdrawing = true";
							maps.append( new Map( mapcurrenttype, mapcurrentcolor ) );
							_currentmap = maps.last();
							_currentmap->_points.append( new Point ( _currentmap, realpos ) );
							_currentmap->_points.append( new Point ( _currentmap, realpos ) );
							_currentpoint = _currentmap->_points.last();
							drawSegment();
						}
					} else {	 // a control point was selected
						qDebug() << "_currentpoint is nearest found.";
						isdrawing = true; // start to draw
						qDebug() << "isdrawing = true";
						ismoving = true;	// start to move
						i = findMap( _currentpoint->_parentmap ); // search current map
						_currentmap = maps.at(i); // store the map pointer
						_currentmap->ismoved = true; // change state of map
						_currentpoint->xy = realpos;	 // update coordinates
						if ( _currentmap->type == 'P' ) { // polygon
							drawMapExcept( _currentpoint );
						}
					}
				} else { // isdrawing is true
					_currentpoint->xy = realpos; // update coordinates of last point
					if ( ismoving ) {	// finish to move a point
						isdrawing = false;	 // stop to draw
						ismoving = false; // stop to move
						_currentmap->ismoved = false; // change state of map
						repaint();
					} else { // drawing something
						if ( _currentmap->type == 'P' ) { // polygon
							drawSegment();
							_currentmap->_points.append( new Point ( _currentmap, realpos ) );
							_currentpoint = _currentmap->_points.last();
							drawSegment();
						} else { // not a polygon : finish to draw
							isdrawing = false;	 // stop to draw
							repaint();
						}
					}
				}
			}

			if ( event->button() == Qt::RightButton ) {
				if ( isdrawing == false ) { // not currently drawing
					popupMenuMapType->exec( QCursor::pos() ); // popup menu
				} else { // isdrawing = true
					if ( _currentmap->type == 'P' ) { // close the polygon
						isdrawing = false;	 // stop to draw
						if ( _currentmap->_points.count() >= 3 ) { // we need 3 points for a polygon
							repaint();
						} else {
							del( _currentmap );
							repaint();
						}
					}
				}
			}

			if ( event->button() == Qt::MidButton )
				isscrolling = true;
		}
	}

	// always record last mouse position
	lastmousepos = event->pos();
}

/** start to grab a map */
void Canvas::grab ( Map * _map ) {
	if ( _map != NULL ) {
		isgrabbing = true;
		isdrawing = ismoving = false;
		_map->isgrabbed = true;
		// update html field with values from selected map
		emit signalChangeText( _map->htmlfields );
		emit signalStatus1( tr("Map selected") );
	}
}

/** select all maps */
void Canvas::grab () {
	if ( maps.count() ) {
		for ( int i=0; i<int( maps.count() ); i++ )
			grab( maps.at(i) );
		repaint();
		emit signalStatus1( tr("Maps selected") );
	}
}

/** deselect all maps */
void Canvas::ungrab () {
	if ( maps.count() ) {
		for ( int i=0; i<int( maps.count() ); i++ )
			ungrab( maps.at(i) );
		// clean redraw
		isgrabbing = isdrawing = ismoving = isscrolling = false;
	}
	emit signalStatus1( tr("No selection") );
}

/** stop to grab a map */
void Canvas::ungrab ( Map * _map ) {
	_map->isgrabbed = false;
	emit signalStatus1( tr("Map realease") );
}

/** handle mouse move events */
void Canvas::mouseMoveEvent( QMouseEvent * event ) {
	//qDebug() << "mouseMoveEvent: ";

	setFocus();

	QPoint realpos = event->pos() * 100. / zoomfactor;

	if ( isscrolling ) {
		/** scroll the canvas using last mouse position */
		emit signalMouseScrolling( lastmousepos - event->pos() );
	} else {
		// currently drawing
		if ( isdrawing && _currentpoint!=NULL ) {
			_currentpoint->xy = realpos;
			drawSegment();
		}
		// currently grabbing : tranlate objects
		if ( isgrabbing && event->button() == Qt::LeftButton ) {
			for ( int i=0; i<int( maps.count() ); i++ )
				if ( maps.at(i)->isgrabbed ) {
					for ( int j=0; j<int(maps.at(i)->_points.count()); j++ ) {
						QPoint p = event->pos()-lastmousepos;
						maps.at(i)->_points.at(j)->xy += p * 100. / zoomfactor;
					}
					drawMap( maps.at(i) );
				}
			repaint();
		}
		// show maps if no current actions
		if ( !isdrawing && !isgrabbing && !ismoving && !isscrolling ) {
			Map * _map = isCursorInMap( realpos );
			if ( _map != NULL ) {
				showMapType( _map->type );
				emit signalChangeText( _map->htmlfields );
			}
		}
	}

	// show information about control point
	Point * _point = searchNearestPoint( realpos );
	if ( _point != NULL ) {
		int i = findPoint( _point );
		if ( i >= 0 ) {
			QString s = QString("%1:%2").arg(_point->_parentmap->type).arg(i+1);
			if ( isdrawing && _point->_parentmap->type == 'P' ) // polygon
				s += tr(" - press ESC to finish");
			emit signalStatus2( s );
		}
	}

	// always record last mouse position
	lastmousepos = event->pos();
}

/** update current map type */
void Canvas::slotMapType ( int item ) {
	switch (item) {
		case 1: mapcurrenttype='R'; break;
		case 2: mapcurrenttype='C'; break;
		case 5: mapcurrenttype='E'; break;
		case 4: mapcurrenttype='L'; break;
		case 6: mapcurrenttype='P'; break;
	}
	showMapType( mapcurrenttype );
}

/** search the nearest point using mouse position */
Point * Canvas::searchNearestPoint( QPoint p0 ) {
	Map * _map;
	Point * _nearestpoint = NULL, * _point;
	QPoint p1, p2;
	double d, dmin=3.;

	if ( maps.count() )
		for ( int i=0; i < int( maps.count() ); i++ ) {
			_map = maps.at(i);
			if ( _map->_points.count() )
				for ( int j=0; j < int( _map->_points.count() ); j++ ) {
					_point = _map->_points.at(j);
					p2 = _point->xy;
					p1 = p2 - p0;
					d = sqrt(p1.x()*p1.x()+p1.y()*p1.y());
					if ( d < dmin ) {
						dmin = d;
						_nearestpoint = _point;
					}
				}
		}
	return _nearestpoint;
}

/** redraw all maps */
void Canvas::drawMaps () {
	bufferMaps.fill( Qt::transparent );
	if ( maps.count() )
		for ( int i=0; i < int( maps.count() ); i++ )
			drawMap( maps.at(i) );
}

/** draw a single map */
void Canvas::drawMap ( Map * _map ) {
	if ( _map->_points.count() > 1 ) { // need two points...
		// redraw all points
		for ( int i=0; i<int(_map->_points.count()-1); i++ )
		 drawPoints( _map->_points.at(i), _map->_points.at(i+1) );
		// for polygon, close first and last points
		if ( _map->type == 'P' )
		 drawPoints( _map->_points.first(), _map->_points.last() );
	}
}

/** used for moving a polygon point */
void Canvas::drawMapExcept( Point * _point ) {
	if ( _point != NULL ) {
		Map * _map = _point->_parentmap;
		if ( _map != NULL ) {
			if ( _map->_points.count() ) {
				for ( int i=0; i<int(_map->_points.count()-1); i++ )
					if ( _map->_points.at(i) != _point && _point != _map->_points.at(i+1) )
						drawPoints( _map->_points.at(i), _map->_points.at(i+1) );
			}
			if ( _map->_points.first() != _point && _point != _map->_points.last() )
				drawPoints( _map->_points.first(), _map->_points.last() );
		}
	}
}

/** test if the given point is in an object */
Map * Canvas::isCursorInMap ( QPoint p ) {
	bool ret;
	if ( maps.count() ) {
		for ( int i=0; i < int( maps.count() ); i++ ) {
			Map * _map = maps.at(i);
			if ( _map->_points.count()>1 ) {
				QPoint p1 = _map->_points.at(0)->xy;
				QPoint p2 = _map->_points.at(1)->xy;
				Point * _testpoint = new Point ( NULL, p );
				switch ( _map->type ) {
					case 'R' : ret = _testpoint->inRectangle( p1, p2 ); break;
					case 'C' : ret = _testpoint->inCircle( p1, p2 ); break;
					case 'E' : ret = _testpoint->inEllipseRectangle( p1, p2 ); break;
					case 'L' : ret = _testpoint->inEllipseCenterRadius( p1, p2 ); break;
					case 'P' : ret = _testpoint->inPolygon( _map ); break;
				}
				if ( ret ) return _map;
			}
		}
	}
	return NULL;
}

/** handle key events in canvas */
void Canvas::keyPressEvent ( QKeyEvent * event ) {
	if ( event->modifiers() == Qt::NoModifier ) {
		switch ( event->key() ) {
			// add points to polygon
			case Qt::Key_A: add(); break;
			// delete a point or a map
			case Qt::Key_D: del(); break;
			// give focus to HREF
			case Qt::Key_I: edit(); break;
			// abort selection
			case Qt::Key_Escape: abort(); break;
			// scrollings
			case Qt::Key_Up:	 signalScrollBy( QPoint(0,-20) ); break;
			case Qt::Key_Down: signalScrollBy( QPoint(0,20) ); break;
			case Qt::Key_Left: signalScrollBy( QPoint(-20,0) ); break;
			case Qt::Key_Right:signalScrollBy( QPoint(20,0) ); break;
			case Qt::Key_PageUp:
			 signalScrollBy( QPoint( 0,-qMax( height()/10,10) ) );
				break;
			case Qt::Key_PageDown:
			 signalScrollBy( QPoint( 0, qMax(height()/10,10) ) );
				break;
		}
	}
}

/** update text to _htmlfield i using selected maps */
void Canvas::updHtmlFieldForSelectedMaps ( int j, QString text ) {
	// update all selected maps
	for ( int i=0; i<int( maps.count() ); i++ )
		if ( maps.at(i)->isgrabbed )
			maps.at(i)->htmlfields[j] = text;
}

/** handle mouse release buttons events */
void Canvas::mouseReleaseEvent ( QMouseEvent * event ) {
	if ( event->button() == Qt::MidButton )
		isscrolling = false;
	// always record last mouse position
	lastmousepos = event->pos();
}

/** return current map index */
int Canvas::findMap ( Map * _map ) {
	for ( int i=0; i<int(maps.count()); i++ )
		if ( maps.at(i) == _map )
			return i;
	return -1;
}

/** return current point index */
int Canvas::findMap ( Point * _point ) {
	Map * _map = _point->_parentmap;
	for ( int i=0; i<int(_map->_points.count()); i++ )
		if ( _map->_points.at(i) == _point )
			return i;
	return -1;
}

/** return current point index */
int Canvas::findPoint( Point * _point ) {
	Map * _map = _point->_parentmap;
	if ( findMap( _map ) != -1 ) {
		for ( int i=0; i<int(_map->_points.count()); i++ )
			if ( _map->_points.at(i) == _point )
				return i;
	}
	return -1;
}

/** return a copy of background sized to current zoom factor */
void Canvas::zoom ( double newzoomfactor ) {
	//qDebug() <<	"Zoom: ";
	zoomfactor = newzoomfactor;
	QString s = QString("Zoom: %1%").arg((int)zoomfactor);
	emit signalStatus1( s );
	repaint();
}

/** delete a map */
void Canvas::del ( Map * _map ) {
	int i;
	i = findMap( _map );
	if ( i != -1 ) {
		maps.removeAt( i );
		// clean redraw
		isgrabbing = isdrawing = ismoving = false;
		emit signalStatus1( tr("Map deleted") );
	}
}

/** delete a polygonal point */
void Canvas::del ( Point * _point ) {
	if ( _point != NULL ) {
		Map * _map = _point->_parentmap;
		_map->_points.removeAll( _point );
		// for paintEvent
		_currentmap = NULL;
		_currentpoint = NULL;
		// clean redraw
		isgrabbing = isdrawing = ismoving = false;
		emit signalStatus1( tr("Point deleted") );
	}
}

/** only if one map is grabbed */
void Canvas::gotoMap ( int n ) {
	Map * _map = NULL;
	int i,j;
	qDebug() << "gotoMap:" << n;
	if ( !isdrawing && maps.count() ) {
		// user ask for first of last map
		if ( n==0 || n==3 ) {
			// ungrab all maps
			ungrab();
			switch (n) {
				case 0 : _map = maps.first(); break;
				case 3 : _map = maps.last(); break;
			}
		} else {
			// count the number of maps
			j = 0;
			for ( i=0; i<int( maps.count() ); i++ )
				if ( maps.at(i)->isgrabbed ) {
					_map = maps.at(i);
					j++;
				}
			if ( j==1 ) { // only one map grabbed
				ungrab( _map );
				i = findMap( _map );
				switch (n) {
				case 0 : _map = maps.first(); break;
				case 1 : _map = i==0 ? maps.last() : maps.at(i-1); break;
				case 2 : _map = i==int(maps.count() - 1) ? maps.first() : maps.at(i+1); break;
				case 3 : _map = maps.last(); break;
				}
			}
		}
		if (_map != NULL) {
			grab( _map );
			repaint();
			QPoint c(0,0); // gravity center
			for ( i=0; i<int( _map->_points.count() ); i++ )
				c += _map->_points.at(i)->xy;
			c /=	int(_map->_points.count());
			emit signalCenterView( (double)(c.x()), (double)(c.y()) );
		}
	}
}

/** save current map */
bool Canvas::saveMaps2Kmap ( QString kmapfile, QString pictfile ) {
	qDebug() << "saveMaps2Kmap:" << kmapfile;
	QFile file(kmapfile);
	int i,j;

	if ( !file.open( QIODevice::WriteOnly ) )
		return false;

	QTextStream ts( &file );
	// write KMAP file
	if ( !pictfile.isEmpty() )
		ts << "PICTURE=" << pictfile << "\n\n";
	if ( maps.count() ) {
		for ( i=0; i<int( maps.count() ); i++ ) {

			switch ( maps.at(i)->type ) {
				case 'R': ts << "RECTANGLE" << "\n"; break;
				case 'C': ts << "CIRCLE" << "\n"; break;
				case 'E': ts << "ELLIPSERECTANGLE" << "\n"; break;
				case 'L': ts << "ELLIPSECENTERRADIUS" << "\n"; break;
				default : ts << "POLYGON" << "\n";
			}

			ts << maps.at(i)->color.name() << "\n";

			for ( j=0; j<int( maps.at(i)->_points.count() ); j++ ) {
				QPoint p = maps.at(i)->_points.at(j)->xy;
				ts << p.x() << "," << p.y();
				if ( j != int( maps.at(i)->_points.count() -1 ) )
					ts << ",";
			}

			ts << "\n";

			for ( j=0; j<nblabels; j++ )
				if ( maps.at(i)->htmlfields.at(j).length() )
					ts << htmllabel[j] << "=" << maps.at(i)->htmlfields.at(j) << "\n";

			ts << "\n";
		}
		file.close();
		return true;
	}
	return false;
}

/** read maps from HTML */
bool Canvas::loadHtmlFile ( QString htmlfile ) {
	qDebug() << "loadHtmlFile: ";
	QFile file( htmlfile );
	QString line;
	if ( !file.open( QIODevice::ReadOnly ) )
		return false;
	QTextStream ts( &file );
	char t;
	QRegExp pict ("^<img src=\"([^\"]+)\".*$");
	QRegExp area ("^<area shape=\"([^\"]+)\" ([^>]+)>$");
	while ( !ts.atEnd() ) {
		line = ts.readLine(); // line of text excluding '\n'
		qDebug() << line;
		if ( line.isEmpty() )
			continue;
		if ( pict.indexIn( line ) != -1 ) {
			qDebug() << "image: " << line;
			emit signalLoadPict( pict.cap(1) );
			continue;
		}
		if ( area.indexIn( line ) != -1 ) {
			t = '\0';
			qDebug() << "area: " << line;
			if ( area.cap(1)=="rect" )
				t = 'R';
			if ( area.cap(1)=="circle" )
				t = 'C';
			if ( area.cap(1)=="poly" )
				t = 'P';
			if ( t != '\0' ) {
				Map * _map = new Map( t, Qt::black );
				if ( _map->readFromHtml( t, area.cap(2) ) )
					maps.append( _map );
			}
		}
	}
	file.close();
	reloadFormFieldSet();
	repaint();
	return true;
}

/** display new fieldsets according to current maps */
void Canvas::reloadFormFieldSet () {
	for ( int j=0; j<int( maps.count() ); j++ ) {
		Map * _map = maps.at(j);
		// qDebug() << "reloadFormFieldSet:" << j;
		for ( int i=0; i<nblabels; i++ )
			if ( _map->htmlfields[i].length() )
				emit(signalShowFormFieldSet(i));
	}
}

/** save maps to HTML */
bool Canvas::saveMaps2Html ( QString htmlfile, QString pictfile ) {
	QFile file( htmlfile );
	if ( !file.open( QIODevice::WriteOnly ) )
		return false;
	QTextStream ts( &file );
	if ( !pictfile.isEmpty() )
		ts << "<img src=\"" << pictfile << "\" alt=\"map\" usemap=\"#kmap\" />\n";
	ts << "<map name=\"kmap\" id=\"kmap\">\n";
	if ( maps.count() )
		for ( int i=0; i<int( maps.count() ); i++ )
			ts << maps.at(i)->convert2Html() << "\n";
	ts << "</map>\n";
	file.close();
	return true;
}

/** save maps to XML */
bool Canvas::saveMaps2Xml ( QString xmlfile, QString pictfile ) {
	qDebug() << "saveMaps2Xml:" << xmlfile;
	QFile file(xmlfile);
	int i,j;

	if ( !file.open( QIODevice::WriteOnly ) )
		return false;

	QTextStream ts( &file );

	QXmlStreamWriter stream(&file);
	stream.setAutoFormatting(true);
	stream.writeStartDocument();

	stream.writeStartElement("kmap");

	if ( !pictfile.isEmpty() ) {
		stream.writeStartElement("img");
		stream.writeAttribute("src", pictfile);
		stream.writeEndElement();
	}

	if ( maps.count() )
		for ( i=0; i<int( maps.count() ); i++ ) {
			stream.writeStartElement("area");
			QString type;
			switch ( maps.at(i)->type ) {
				case 'R': type = "rectangle"; break;
				case 'C': type = "circle"; break;
				case 'E': type = "ellipse_rectangle"; break;
				case 'L': type = "ellipse_center_radius"; break;
				default : type = "polygon";
			}
			stream.writeAttribute("type", type);
			stream.writeAttribute("color", maps.at(i)->color.name());
			for ( j=0; j<nblabels; j++ )
				if ( maps.at(i)->htmlfields.at(j).length() )
					stream.writeAttribute( htmllabel[j].toLower(), maps.at(i)->htmlfields.at(j) );
			for ( j=0; j<int( maps.at(i)->_points.count() ); j++ ) {
				QPoint p = maps.at(i)->_points.at(j)->xy;
				stream.writeStartElement( "point" );
				stream.writeAttribute( "x", QString::number(p.x()) );
				stream.writeAttribute( "y", QString::number(p.y()) );
				stream.writeEndElement();
			}
			stream.writeEndElement();
		}
	stream.writeEndDocument();
	file.close();
	return true;
}

/** read maps from a kmap file */
bool Canvas::loadKmapFile ( QString kmapfile ) {
	QFile file(kmapfile);
	QString line;
	QStringList coords;
	char t = '\0';
	QColor c;
	int i,x,y;
	Map * _map = NULL;
	bool ok;
	if ( !file.open( QIODevice::ReadOnly ) )
		return false;
	QTextStream ts( &file );
	while ( !ts.atEnd() ) {
		line = ts.readLine(); // line of text excluding '\n'
		if ( line.isEmpty() )
			continue;
		QRegExp pict ("^PICTURE=(.*)$");
		if ( pict.indexIn( line ) != -1 ) {
			qDebug() << "image: " << line;
			emit signalLoadPict( pict.cap(1) );
			continue;
		}
		if ( line == "RECTANGLE" ) t = 'R';
		if ( line == "CIRCLE" ) t = 'C';
		if ( line == "ELLIPSERECTANGLE" ) t = 'E';
		if ( line == "ELLIPSECENTERRADIUS" ) t = 'L';
		if ( line == "POLYGON" ) t = 'P';
		QRegExp hexacolor ("^#[a-zA-Z0-9]$");
		if ( hexacolor.indexIn( line ) != -1 )
			c.setNamedColor(line);
		QRegExp coordinates ("^[-\\d,]+$");
		if ( coordinates.indexIn( line ) != -1 ) {
			coords = line.split( "," );
			maps.append( new Map( t, c ) );
			_map = maps.last();
			for ( i=0; i<int(coords.count()); i+=2 ) {
				x = coords[i].toInt( &ok );
				y = coords[i+1].toInt( &ok );
				_map->_points.append( new Point ( _map, QPoint(x,y) ) );
			}
		}
		QRegExp hash ("^([a-zA-Z]+)=(.*)$");
		if ( hash.indexIn( line ) != -1 ) {
			if ( _map != NULL )
				for ( i=0; i<nblabels; i++ )
					if ( hash.cap(1).toLower() == htmllabel[i].toLower() )
						_map->htmlfields[i] = hash.cap(2);
		}
	}
	file.close();
	reloadFormFieldSet();
	repaint();
	return true;
}

/** copy selected maps and translate result */
void Canvas::copy () {
	// copy grabbed maps
	if ( isgrabbing ) {
		for ( int i=int( maps.count() -1 ); i>=0; i-- )
			if ( maps.at(i)->isgrabbed ) {
				Map * _map = maps.at(i);
				ungrab( maps.at(i) );
				maps.append( new Map( _map->type, _map->color ) );
				Map * m = maps.last();
				for ( int j=0; j<int( _map->_points.count() ); j++ )
					m->_points.append( new Point ( m, _map->_points.at(j)->xy + QPoint(40,20) ) );
				for ( int j=0; j<nblabels; j++ )
					m->htmlfields[j] = _map->htmlfields[j];
				grab( m );
			}
		repaint();
		emit signalStatus1( tr("Copy -> Ok") );
	}
}

/** select all maps */
void Canvas::select () {
	if ( !isdrawing && !ismoving && !isscrolling ) {
		grab();
		repaint();
	}
}

/** delete polygonal point or full maps according to context */
void Canvas::del () {
	QPoint realpos = mapFromGlobal( QCursor::pos() ) * 100. / zoomfactor;
	if ( isgrabbing ) { // one or several map selected
		for ( int i=int( maps.count() -1 ); i>=0; i-- )
			if ( maps.at(i)->isgrabbed )
				del( maps.at(i) );
		repaint();
	} else {	// try to delete a single point
		// no actions needed
		if ( !isdrawing && !ismoving && !isgrabbing && !isscrolling ) {
			Point * _point = searchNearestPoint( realpos );
			if ( _point != NULL ) { // control point selected
				Map * _map = _point->_parentmap;
				if ( _map->type!='P'
					|| (_map->type=='P' && _map->_points.count()<4) ) // delete map
						del( _map );
				else	// polygon with more than 3 points
					del( _point );
				repaint();
			}
		}
	}
}

/** add points to polygon */
void Canvas::add () {
	QPoint realpos = mapFromGlobal( QCursor::pos() ) * 100. / zoomfactor;
	Point * _p;
	int j;
	// no actions needed
	if ( !isdrawing && !ismoving && !isgrabbing && !isscrolling ) {
		Point * _point = searchNearestPoint( realpos );
		if ( _point != NULL ) { // control point founded
			Map * _map = _point->_parentmap;
			if ( _map->type == 'P' ) {
				j = findPoint( _point );
				if ( j )
					_p = new Point ( _map, (_point->xy + _map->_points.at(j-1)->xy)/2 );
				else
					_p = new Point ( _map, (_point->xy + _map->_points.last()->xy)/2 );
				_map->_points.insert( j, _p );
				j = findPoint( _point );
				if ( j < int(_map->_points.count()-1) )
					_p = new Point ( _map, (_point->xy + _map->_points.at(j+1)->xy)/2 );
				else
					_p = new Point ( _map, (_point->xy + _map->_points.first()->xy)/2 );
				_map->_points.insert( j+1, _p );
				repaint();
				emit signalStatus1( tr("Points added") );
			}
		}
	}
}

/** show or hide control points */
void Canvas::slotControlPoints ( bool b ) {
	hidecontrolpoint = !hidecontrolpoint;
	if ( !isdrawing && !ismoving ) {
		repaint();
	}
	if ( hidecontrolpoint )
		signalStatus1( "Hide control points" );
	else
		signalStatus1( "Show control points" );
}

/** handle actions on selected maps */
void Canvas::slotMapAction ( int i ) {
	switch ( i ) {
		case 1: select(); break; // select all map
		case 2: del(); break; // delete selected maps
		case 3: copy(); break; // copy selected maps
		case 4: gotoMap(0); break; // go to first map
		case 5: gotoMap(1); break; // go to prev map
		case 6: gotoMap(2); break; // go to next map
		case 7: gotoMap(3); break; // go to last map
		case 8: edit(); break; // go to edit mode
	}
}

/** handle mouse wheel events */
void Canvas::wheelEvent ( QWheelEvent * event ) {
	QPoint p(0, 0);
	event->accept();
	if ( event->modifiers() == Qt::NoModifier )
		p = QPoint( 0, -event->delta() );
	if ( event->modifiers() == Qt::AltModifier )
		p = QPoint( event->delta(), 0 );
	emit signalScrollBy ( p );
	repaint();
}

/** show current map type in status2 */
void Canvas::showMapType ( char c ) {
	switch (c) {
		case 'R': emit signalStatus2( "Rectangle" ); break;
		case 'C': emit signalStatus2( "Circle (center+radius)" ); break;
		case 'E': emit signalStatus2( "Ellipse (rectangle)" ); break;
		case 'L': emit signalStatus2( "Ellipse (center+radius)" ); break;
		case 'P': emit signalStatus2( "Polygon" ); break;
	}
}

/** update the last segment of the current map */
void Canvas::drawSegment () {
	if ( _currentmap != NULL )
		if ( _currentpoint != NULL ) {
				int i = findPoint( _currentpoint );
				if ( i != -1 ) {
					// if current point is not the first one or the last one,
					// draw the two respective segments
					if ( i > 0 )
						drawPoints( _currentmap->_points.at(i-1), _currentmap->_points.at(i) );
					if ( i < int(_currentmap->_points.count()-1) )
						drawPoints( _currentmap->_points.at(i), _currentmap->_points.at(i+1) );
					// for polygon, draw the segment form the first to the last point
					if ( _currentmap->type == 'P' && ismoving )
						if ( i==0 || i==int(_currentmap->_points.count()-1) )
						 drawPoints( _currentmap->_points.first(), _currentmap->_points.last() );
				}
		}
	repaint();
}

/** go to edit mode after a keypress event */
void Canvas::edit () {
	if ( isgrabbing && !ismoving )
		emit signalFocusText();
}

/** abort current action */
void Canvas::abort () {
	// delete polygon if nbpoints<3...
	if ( isdrawing && mapcurrenttype=='P' ) {
		_currentmap = maps.last();
		if (_currentmap->_points.count() < 3 )
			del( _currentmap );
	}
	ungrab();
	repaint();
}

/** print current maps to printer */
void Canvas::printMaps() {

	qDebug() << "printMaps:";

	QPrinter printer(QPrinter::HighResolution);

	printer.setFullPage( true );
	printer.setPageSize( QPrinter::A4 );
	printer.setOrientation( QPrinter::Portrait );
	printer.setColorMode( QPrinter::Color );
	printer.setDocName( "KMap" );
	printer.setCreator( "KMap" );
	printer.setOutputFileName( "/tmp/kmap.pdf" );
	printer.setResolution( 300 ); // dpi

//
	int w = (double)qMax( bufferPicture.width(), bufferMaps.width() );
	int h = (double)qMax( bufferPicture.height(), bufferMaps.height() );

	if ( printer.setup(this) ) {
		QPainter painter;
		if( painter.begin( &printer ) ) {
			double xscale = printer.pageRect().width()/w;
			double yscale = printer.pageRect().height()/h;
			double scale = qMin(xscale, yscale);
			painter.translate(printer.paperRect().x() + printer.pageRect().width()/2,
														printer.paperRect().y() + printer.pageRect().height()/2);
			painter.scale(scale, scale);
			painter.translate(-w/2, -h/2);

			//QFont font( "arial", 12 );
			//QFontMetrics fm = painter.fontMetrics();

			qDebug() << "printMaps2";
			painter.drawPixmap( 0, 0, bufferPicture );
			drawMaps();
			painter.drawPixmap( 0, 0, bufferMaps );
			qDebug() << "end printing";
			painter.end();
		}
	}
}

Conclusion

L'actualisation de KMap en 2012 m'a permis de voir les changements opérés par Nokia entre Qt2 et Qt4. J'avais déjà fort apprécié QtCreator et QtDesigner il y a plus de 10 ans, et force est de constater que ces outils sont toujours aussi finalisés et puissants que par le passé !

Face aux «GUI mammouths» de type Eclipse, la librairie Qt, aujourd'hui reprise par la société Digia, est sans conteste l'un des fleurons de conception en C++, sans compter que Qt est multi-OS, et très largement usitée en embarqué !