KMap - un outil d'Image Mapping pour KDE
KMap - 2.0 - (Image Mapping Tool) - Jean Luc Biellmann - 2012
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.
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("""), 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(""") );
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é !