====== Partie VI – Objets graphiques ====== [[in204:tds:sujets:td1|TD1]] ** A effectuer en dehors du cours ** Cette partie vise à définir des classes pour des objets graphiques. La plupart des bibliothèques d’interfaces graphiques sont écrites dans des langages orientés objets, qui sont particulièrement adaptés pour. Dans cette partie, nous définissons les classes de points, de segment et de droite dans le plan. ==== Question n°1 ==== En vous inspirant du code fourni, écrivez, compilez et mettez au point en C++ * Une classe ''Point'', comprenant une abscisse entière et une ordonnée entière et donnant un accès à ces champs. * Une classe ''Segment'' constituée de deux extrémités sous la forme de ''Point''. Un segment est construit à l’aide de deux points et doit fournir les méthodes suivantes * une méthode pour tester si un point appartient au segment. * une méthode qui teste l’intersection du segment avec un autre segment. Cette méthode retourne vrai s’il existe une l’intersection ainsi que le point d’intersection faux, dans le cas contraire. * Une classe Droite constituée d’un Point et d’un vecteur indiquant la direction, vecteur également constitué d’une partie abscisse et d’une partie ordonnée et doit fournir les méthodes suivantes * un constructeur à partir d’un segment, * un constructeur à partir d’un point et d’un angle * un constructeur à partir de deux points * une méthode qui teste l’intersection de la droite avec une autre droite. Cette méthode retourne vrai s’il existe une l’intersection ainsi que le point d’intersection faux, dans le cas contraire. * une méthode qui teste l’intersection de la droite avec un segment. Cette méthode retourne vrai s’il existe une l’intersection ainsi le point d’intersection faux, dans le cas contraire. La classe ''Point'' définit deux champs ''m_x'' et ''m_y'' qui sont déclarés comme étant privés, ie. uniquement visible des méthodes définies dans la classe. Il est habituel de préfixer les champs membres par un indicateur qui indique que ces champs appartiennent à la classe. Il existe plusieurs notations : * on préfixe le champ par ''m_'', donc ''x'' devient ''m_x'', * on préfixe le champ par ''_'', voire ''__'', donc ''x'' devient ''__'' Dans le corrigé, nous avons adopter la première notation, cependant, ce n'est qu'indicatif. Dans le cas présent, nous n'implantons pas de [[in204:cpp:syntax:class:constructor:default|constructeur par défaut]], nous considérons qu'il est nécessaire de fournir une abcisse et une ordonnée pour pouvoir initialiser une objet de type ''Point''. Ceci est un choix d'implantation et d'autres implantations peuvent considérer que l'initialisation d'un objet de type ''Point'' par défaut consiste à initialiser les champs ''m_x'' et ''m_y'' avec la valeur ''0''. Nous implantons de manière systématique le [[in204:cpp:syntax:class:constructor:copy|constructeur de recopie]]. Ceci est un choix. En pratique, nous aurions pu ne pas implanter le constructeur de recopie mais il est préférable de savoir ce que ce constructeur fait effectivement. Nous implantons enfin un [[in204:cpp:syntax:class:constructor:specialized|constructeur spécialisé]] qui construit un object ''Point'' à partir d'une valeur abscisse et d'une valeur ordonnée. Comme les champs sont privés, nous définissons deux méthodes d'accès en lecture (getter methods) qui retourne les valeurs des champs ''m_x'' et ''m_y''. Ceci est un moyen de garantir que les valeurs des champs ne seront jamais modifiées. class Point { private: int m_x; int m_y; public: Point(int x, int y): m_x(x), m_y(y) {} Point(const Poinst& anotherPoint): m_x(anotherPoint.m_x), m_y(anotherPoint.m_y) {} int getX() const { return m_X; } int getY() const { return m_Y; } }; La classe ''Segment'' constituée de deux extrémités sous la forme de ''Point''. Ces deux points sont définis par les champs ''m_start'' et ''m_end''. Nous définissons [[in204:cpp:syntax:class:constructor:copy|le constructeur de recopie]] ainsi que deux constructeurs spécialisés construisant un segment soit à partir des points extrémaux, soit au contraire à partir des abscisses et ordonnés des points extrémaux. une l’intersection ainsi que le point d’intersection faux, dans le cas contraire. class Segment { private: Point m_start; Point m_end; public: Segment(const Point& theStart, const Point& theEnd): m_start(theStart), m_end(theEnd) {} Segment(int startPointX, int startPointY, int endPointX, int endPointY): m_start(startPointX, startPointY), m_end(endPointX, endPointY) {} Segment(const Segment& anotherSegment): m_start(anotherSegment.m_start), m_end(anotherSegment.m_end) {} bool contains(Point aPoint) { if(m_start.getX() == m_end.getX()) { if(aPoint.getX() != m_start.getX()) return false; if(m_start.getY() <= m_end.getY()) { if(aPoint.getY() < m_start.getY() || aPoint.getY() > m_end.getY()) return false; } else { if(aPoint.getY() > m_start.getY() || aPoint.getY() < m_end.getY()) return false; } return true; } double ratio = (double)(m_start.getY() - m_end.getY() / (double)(m_start.getX() - m_end.getX()); int pointY = (int)floor(ratio * (aPoint.getX() - m_end.getX()) + m_end.getY(); return pointY == aPoint.getY(); } bool intersectsWith( const Segment& anotherSegment, Point& theIntersectingPoint) { Droite line(m_start, m_end); Droite anotherLine(anotherSegment.m_start, anotherSegment.m_end); double intersectionX, intersectionY; if(!line.intersectsWith( anotherLine, intersectionX, intersectionY)) return false; theIntersectingPoint = Point( (int)round(intersectionX), (int)round(intersectionY)); return contains(theIntersectingPoint ) || anotherSegment.contains(theIntersectingPoint); } }; class Droite { private: Point m_point; double m_angle; public: Droite(double anAngle, const Point& aPoint): m_point(aPoint), m_angle(anAngle) {} Droite(const Point& theStart, const Point& theEnd): m_point(theStart), m_angle(atan2( (double)(theEnd.getY() - theStart.getY()) (double)(theEnd.getX() - theStart.getX()))) {} Droite(const Line& anotherLine): m_point(anotherLine.m_point), m_angle(anotherLine.m_angle) {} bool intersectsWith(const Line& anotherLine) { double intersectionX; double intersectionY; return intersectsWith(anotherLine, intersectionX, intersectionY); } bool intersectsWith( const Line& anotherLine, double& theIntersectionX, double& theIntersectionY) { if(m_point.getX() == anotherLine.m_point.getX() && m_point.getY() == anotherLine.m_point.getY()) return true; if(m_angle == anotherLine.m_angle) { double angle = atan2( anotherLine.m_point.getY() - m_point.getY(), anotherLine.m_point.getX() - m_point.getX()); if(angle != m_angle) return false; } return true; } } ==== Question n° 2 ==== Ajouter à toutes ces classes des méthodes d’affichage pour le déboguage que l’on appellera ''print''. class Point { // Ajouter la méthode suivante après les autres // méthodes. public: void print() const { std::cout << "(" << m_x << ", " << m_y << ")"; } }; class Segment { // Ajouter la méthode suivante après les autres // méthodes. public: void print() const { std::cout << "["; m_start.print(); std::cout << "-"; m_end.print(); std::cout << "]"; } }; class Droite { // Ajouter la méthode suivante après les autres // méthodes. public: void print() const { std::cout << "<"; m_point.print(); std::cout << ", " m_angle << ">"; } }; ==== Question n° 3 ==== A la classe ''Droite'' * Ajouter une méthode rotate90() effectuant une rotation de 90 degrés à une droite. * Ajouter une méthode move(int x, int y) effectuant une translation à une droite. Cette version correspond à des méthodes qui crée un nouvel objet qui est une copie de l'objet courant et ne modifie par l'objet courant. ... const double pi = 3.14; ... class Droite { // Code précédent de la classe. ... public: Droite rotate(double anAngle) const { return Droite(m_Point, m_angle + anAngle); } Droite rotate90(double anAngle) const { return rotate(m_Point, m_angle + 3.14/4); } Droite move(int x, int y) const { return Droite( Point(m_Point.getX() + x, m_Point.GetY() + y), m_angle); } ... }; Une autre possibilité consiste à modifier l'objet courant. C'est ce que fait le code suivant : ... const double pi = 3.14; ... class Droite { ... // Code précédent de la classe. public: Droite& rotate(double anAngle) { m_angle += anAngle; return *this; } Droite& rotate90(double anAngle) { return Turn(m_Point, m_angle + pi/4); } Droite& move(int x, int y) { mPoint = Point(m_Point.getX() + x, m_Point.GetY() + y); return *this; } }; Typiquement, nous retournons une référence à l'objet que nous venons de modifier. Ceci permet d'enchaîner les transformations comme suit : Droite droite(Point(0,0), pi/8); droite.Move(2,3).rotate(-pi/8); // Nous effectuons une droite et puis nous effectuons // une rotation. ==== Question n° 4 ==== Tester votre classe sur le code suivant : #include int main() { Droite d1(Point(0,0), Point(4, 2)); Droite d2(d1); d1.print(); std::cout << std::endl; d2.print(); std::cout << std::endl; d1.rotate90(); d1.print(); std::cout << std::endl; d2.print(); return 0; } Il suffit d'exécuter le code pour les classes réalisées et vérifier que les résultas sont conformes aux résultats attendus.