Editeur de Points - John COLIBRI. |
- résumé : outil de dessin de liste de points, avec ajout, suppression, déplacement, étirements, interpolation
- mots clé : graphique - lignes brisées - translation, homothétie
- logiciel utilisé : Windows XP, Delphi 6.0
- matériel utilisé : Pentium 1.400Mhz, 256 M de mémoire
- champ d'application : Delphi 1 à 2005 sur Windows
- niveau : développeur Delphi
- plan :
1 - Introduction
Pour le programme de correction de couleurs nous souhaitions laisser l'utilisateur dessiner des courbes de corrections qu'il
souhaitait appliquer. Nous avions commencé par des courbes de Bézier, mais il s'avéra que pour des images de 256* 256 le calcul des
interpolations des 65.536 points prenait une dizaine de secondes. Le calcul des cubes de 4 points, et ceci répété plusieurs fois pour interpoler les points de la courbe, ralentissait le traitement.
Nous avons alors décidé d'utiliser plutôt des lignes brisées, où le calcul d'interpolation se résume à la recherche du segment puis une simple règle de trois. L'ensemble a alors été encapsulé dans un éditeur permettant d'ajouter, retirer,
déplacer les points, la partie correction de point utilisant la ligne brisée (ou la courbe de Bézier) correspondante pour modifier les couleurs de la bitmap. Dans cet article, nous allons présenter l'éditeur de points.
2 - Lignes brisées Le principe de base est simple: à chaque clic souris nous ajoutons un point à une liste de points. Le principal problème est de définir les fonctionalités supplémentaires (ajout,
déplacement...) et de spécifier une ergonomie raisonable. Pour les fonctionalités, nous souhaitons: - ajouter des points, bien évidemment. Mais comme nous souhaitons définir des
fonctions pour nos calculs de corrections d'image (un seul y pour chaque x), les points serons insérés dans la liste qui conservera les points triés dans l'ordre des x croissants (ceci pourrait être contrôlé par un
booléen)
- déplacer un point: le clic "à proximité" d'un point sélectionne ce point. Le mouvement de la souris déplace alors le point.
Pour cela, il faut pouvoir distinguer entre le "clic d'insertion" et le
"clic de déplacement". Nous avons choisi de travailler en utilisant un "mode ajout" et un "mode déplacement" - la frappe de la touche "Suppr" supprime le point sélectionne (sans Undo)
- déplacer plusieurs points ensembles: nous utilisons un rectangle de sélection (sorte de lasso), et le déplacement du rectangle déplace les points contenus dans le rectangle. Ici aussi il faut pouvoir distinguer le
"clic point" du "clic rectangle". Nous avons spécifié que:
- le rectangle n'est modifiable qu'en mode déplacement (mais reste visible en mode ajout, pour éventuellement servir de guide de positionnement)
- en mode déplacement, les clics loin de points permet de créer le rectangle (par tirer-glisser), puis les clics au voisinage du rectangle ou dans le rectangle permettent de le redimensionner ou le déplacer
- le clic droit souris déplace les points qui sont dans le rectangle. Si le rectangle change de taille, les points sont déplacés de façon proportionnelle à leur position dans le rectangle (étirement ou rétrécissement)
- le déplacement de points individuels reste possible
3 - Le programme Delphi 3.1 - u_c_2d_c_point_list - Les listes de points
Chaque point contient des coordonées x et y réelles et un nom (en général vide). Nous avons utilisé des réels car nos traitements d'image se font sur des réels. Voici la définition ce c_2d_point:
c_2d_point= Class(c_basic_object)
m_x, m_y: Double;
constructor create_2d_point(p_name: String; p_x, p_y: Double);
function f_display_2d_point: String; Virtual;
procedure display_2d_point; Virtual;
function f_c_self: c_2d_point;
procedure draw_2d_point(p_c_canvas: tCanvas; p_pen_color, p_radius: Integer); Virtual;
end; // c_2d_point |
La liste est gérée par notre encapsulation classique de tStringList
c_2d_point_list= Class(c_basic_object)
_m_c_2d_point_list: tStringList;
m_is_closed: Boolean;
Constructor create_2d_point_list(p_name: String);
function f_2d_point_count: Integer;
function f_c_2d_point(p_2d_point_index: Integer): c_2d_point;
function f_index_of(p_2d_point_name: String): Integer;
function f_index_of_c_2d_point(p_c_2d_point: c_2d_point): Integer;
procedure add_2d_point(p_2d_point_name: String; p_c_2d_point: c_2d_point);
procedure insert_2d_point(p_2d_point_name: String; p_c_2d_point: c_2d_point;
p_insertion_point: Integer);
procedure delete_2d_c_point(p_c_2d_point: c_2d_point);
procedure add_and_sort_x_2d_point(p_2d_point_name: String; p_c_2d_point: c_2d_point);
procedure display_2d_point_list; Virtual;
procedure draw_2d_point_list_poly_line(p_c_canvas: tCanvas;
p_pen_color: Integer);
procedure draw_2d_point_list_points(p_c_canvas: tCanvas;
p_pen_color, p_radius: Integer);
Destructor Destroy; Override;
end; // c_2d_point_list |
Mentionnons simplement: - m_is_closed qui permet de fournir le point 0 lorsque nous dépassons la fin de la liste (pour les traitements nécessitant le parcours de toute la liste)
- draw_2d_point_list_poly_line qui dessine la ligne brisée entre les points, alors que draw_2d_point_list_points dessine les points individuels
3.2 - u_c_2d_selection_point_list
La liste précédente n'est utilisée que pour des listes de points réels. Aucune fonctionnalité de sélection et coloriage du point sélectionné. Pour ce type de traitment, nous avons crée des descendants:
c_2d_selection_point= Class(c_2d_point)
m_is_selected: Boolean;
m_radius: Integer;
constructor create_2d_selection_point(p_name: String;
p_x, p_y: Double; p_radius: Integer);
function f_c_self: c_2d_selection_point;
function f_display_2d_point: String; Override;
procedure display_2d_point; Override;
procedure draw_2d_selection_point(p_c_canvas: tCanvas;
p_pen_color: Integer);
end; // c_2d_selection_point
c_2d_selection_point_list= Class(c_2d_point_list)
m_c_2d_selected_point_ref: c_2d_selection_point;
m_c_canvas_ref: tCanvas;
Constructor create_2d_selection_point_list(p_name: String;
p_c_canvas_ref: tCanvas);
procedure draw_selected_point;
function f_c_nearest_point(p_x, p_y: Double): c_2d_selection_point;
procedure display_2d_point_list; Override;
Destructor Destroy; Override;
end; // c_2d_selection_point_list
|
3.3 - L'ajout de points Il suffit à présent de créer un point à chaque clic ce la souris. Nous traitons simplement OnMouseDown et ajoutons le point avec ces coordonnées à la liste.
Pour pouvoir effacer un point, nous gérons dans c_2d_selection_point_list une référence vers un point particulier. En mode ajout, le point courant est naturellement le point ajouté. Il faut alors gérer le transfert de sélection à chaque ajout.
De plus nous affichons la courbe définie par les points. Lorsque nous ajoutons un point, il suffit d'ajouter le dernier segment. Mais lorsque nous insérons un point entre deux points, il faut remplacer un segment par deux segments. Et si
la courbe n'est pas une ligne brisée joignant les points mais une courbe plus complexe (Spline, Bézier), l'insertion d'un point peut modifier une zone allant au-delà de l'intervalle des deux points voisins. Pour cela nous avons préféré
effacer toute la courbe avant une insertion, et recréer la courbe après l'ajout. Pour nos courbes comportant un dizaine de points, cela ne pose aucun problème de performance. Ces divers traitements sont donc dans tPaintBox.OnMouseDown
3.4 - L'effacement de points Le clic sur un tButton efface le point sélectionnée. Il y a ici aussi gestion du transfert du point sélectionné et redessin de la courbe.
3.5 - Le déplacement d'un point
Pour déplacer un point, il faut basculer en mode "déplacement" en cliquant sur un SpeedButton (utilisé en mode CheckBox: il reste enfoncé lorsqu'on le clique).
Le déplacement de la souris est capté dans tPaintBox.OnMouseMove qui permet de modifier les coordonnées du point. Le tout agrémenté d'effacement préalable et redessin postérieur.
3.6 - La sélection multiple
Pour déplacer plusieurs points de concert, ou étirer / contracter leur position, nous utilisons un c_rectangle_selecter. Le rectangle de sélection
peut être défini avec la souris en mode "déplacement", et le déplacement / étirement des points se fait à la souris en utilisant le clic droit
3.7 - Ligne brisée / Bézier
Une tCheckBox permet de basculer entre l'affichage d'une ligne brisée ou d'une courbe de Bézier. Le traitement des courbes de Bézier
a fait l'objet d'un article séparé, mais nous avons laissé la possibilité de les utiliser dans l'éditeur. La boîte d'édition avec le tUpDown étaient aussi prévus pour effectuer des mises à l'échelles pour notre traitement de photographie.
3.8 - Encapsulation dans une classe Au début, nous avons créé l'éditeur dans la tForm principale. L'ensemble fait environ 500 lignes. Cela nous paraissait lourd de recopier le tout dans le projet de correction photographique.
La solution politiquement correcte est de définir un composant. Personnellement je n'utilise guère les composants: la palette est déjà surchargée (scrolling exagéré vers la droite), et chaque changement de version de Delphi et
réinstallation de Delphi nécessite une réinstallation. C'est pourquoi j'utilise des "semi-composants": ce sont des CLASS qui contiennent des références vers un conteneur de la tForm principale, et qui
crééent et gèrent les contrôles dont ils ont besoin. Dans notre cas, du point de vue de programme utilisateur, ce qui serait agréable est de poser un tPanel sur la forme, puis de créer un éditeur qui ira
se nicher en mode alClient dans ce tPanel. L'utilisateur manipule l'éditeur (comme lorsqu'il navigue dans un OpenDialog) et récupère, chaque fois qu'il le souhaite, les points de la courbe, ou les valeurs interpolées à partir ce des
points.
Pour cela, il suffit de créer le composants à la volée, et de les relier au conteneur fourni par la tForm. Par exemple, pour créer un tLabel
- nous définissons une fonction de création avec les paramètres "raisonables":
function f_c_create_label(p_name, p_caption: String;
p_c_owner: tComponent; p_c_parent: tWinControl;
p_align: tAlign; p_left, p_top, p_height: Integer): tLabel;
begin Result:= tLabel.Create(p_c_owner);
with Result do begin
Name:= p_name; Caption:= p_caption;
Parent:= p_c_parent; Align:= p_align;
if p_left<> -1
then Left:= p_left;
if p_top<> -1
then Top:= p_top;
if p_height<> -1
then Height:= p_height;
end; // with l_c_status_panel end; // f_c_create_label
| - l'utilisateur peut appeler, par exemple:
mon_c_label:= f_c_create_label('total_label', 'total:',
Form1, Panel2, alTop, 0, 2, 16); |
Pour notre éditeur, nous avons besoin - d'un tPanel qui contiendra les contrôles (basculement du mode addition au mode déplacement, affichage de segments ou de bézier, numéro de point sélectionnée etc)
- de la tPaintBox qui permettra l'affichage et la modification des points
Nous avons donc créé la classe par:
Constructor c_points_editor.create_points_editor(p_name: String;
p_c_container_panel_ref: tPanel);
var l_running_left: Integer;
function f_left(p_width: Integer): Integer;
begin Result:= l_running_left;
Inc(l_running_left, p_width+ 5);
end; // f_left begin // create_points_editor
Inherited create_basic_object(p_name);
m_c_container_panel_ref:= p_c_container_panel_ref;
// -- add the top control panel
m_c_control_panel:= f_c_create_panel('',
m_c_container_panel_ref, m_c_container_panel_ref,
alTop, -1, 22, clBtnFace);
// -- the controls on the control panel l_running_left:= 5;
m_c_selected_label:= f_c_create_label('label', '-1',
m_c_container_panel_ref, m_c_control_panel,
alNone, f_left(20), 2, 16);
m_c_selected_label.Color:= clYellow;
m_c_reset_button:= f_c_create_button('reset', 'Reset',
m_c_container_panel_ref, m_c_control_panel,
f_left(40), 2, 40, 18);
m_c_reset_button.OnClick:= handle_reset_click;
m_c_add_speed_button:= f_c_create_speed_button('speed', 'add',
m_c_container_panel_ref, m_c_control_panel,
f_left(40), 2, 40, 18, True, 33);
m_c_add_speed_button.OnClick:= handle_speed_button_click;
m_c_delete_button:= f_c_create_button('delete', 'Delete',
m_c_container_panel_ref, m_c_control_panel,
f_left(40), 2, 40, 18);
m_c_delete_button.OnClick:= handle_delete_click;
m_c_bezier_checkbox:= f_c_create_checkbox('bezier', 'bezier',
m_c_container_panel_ref, m_c_control_panel,
f_left(40), 2, 50, 18);
m_c_bezier_checkbox.OnClick:= handle_checkbox_click;
m_c_edit:= f_c_create_edit('scale', '',
m_c_container_panel_ref, m_c_control_panel,
f_left(30), 2, 30, 18);
m_c_edit.OnChange:= handle_edit_change;
m_c_updown:= f_c_create_updown('updown',
m_c_container_panel_ref, m_c_control_panel,
f_left(15), 2, 15, 18, -5, 5, 1, m_c_edit);
// -- add the paintbox
m_c_paintbox:= f_c_create_paint_box('paint',
Nil, m_c_container_panel_ref,
alClient, -1, - 1, clWhite);
with m_c_paintbox do begin
OnMouseMove:= handle_paintbox_mouse_move_event;
OnMouseDown:= handle_paintbox_mouse_down_event;
OnMouseUp:= handle_paintbox_mouse_up_event;
OnPaint:=handle_paintbox_paint; end;
m_c_rectangle_selecter:= c_rectangle_selecter.create_rectangle_selecter('sel',
m_c_paintbox, 5);
m_c_rectangle_selecter.m_is_active:= True;
end; // create_points_editor | Les événements handle_paintbox_mouse_move_event et similaires contiennent
alors tout le traitement d'ajout, effacement, déplacement etc.
La classe c_points_editor est alors définie par:
c_points_editor= class(c_basic_object)
m_c_container_panel_ref: tPanel;
m_c_control_panel: tPanel;
m_c_selected_label: tLabel;
m_c_reset_button: tButton;
m_c_add_speed_button: tSpeedButton;
m_c_delete_button: tButton;
m_c_bezier_checkbox: tCheckBox;
m_c_edit: tEdit;
m_c_updown: tUpDown;
m_c_paintbox: tPaintBox;
m_scale: Double;
m_c_2d_point_list: c_2d_selection_point_list;
m_c_2d_transform_point_list: c_2d_point_list;
m_c_poly_line_transformer: c_transformer;
m_c_rectangle_selecter: c_rectangle_selecter;
Constructor create_points_editor(p_name: String;
p_c_container_panel_ref: tPanel);
procedure create_transform;
procedure handle_reset_click(p_c_sender: tObject);
procedure handle_speed_button_click(p_c_sender: tObject);
procedure handle_delete_click(p_c_sender: tObject);
procedure handle_bezier_checkbox_click(p_c_sender: tObject);
procedure handle_edit_change(Sender: TObject);
procedure handle_paintbox_mouse_move_event(Sender: TObject;
Shift: TShiftState; X, Y: Integer);
procedure handle_paintbox_mouse_down_event(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure handle_paintbox_mouse_up_event(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure handle_paintbox_paint(p_c_sender: tObject);
function f_scaled_bezier_value(p_value: Double): Double;
Destructor Destroy; Override;
end; // c_points_editor |
Notre programme principal se contente alors: - d'importer la classe
- de définir une globale correspondant à cette classe
- de créer l'objet, dans Form.OnCreate, par exemple
uses ... , u_c_points_editor ...
; var g_c_points_editor: c_points_editor= Nil;
procedure TForm1.FormCreate(Sender: TObject);
begin ...
g_c_points_editor:= c_points_editor.create_points_editor('edit',
Panel2); end; // FormCreate |
Voici quelques exemples d'utilisation:
4 - Améliorations Nous n'avons pas cherché à définir l'éditeur ultime. Nous avions à résoudre un problème (dessiner des courbes de transfert) et cet éditeur convient. Il serait aisé d'ajouter, entre autres:
- la sauvegarde disque des points
- la possibilité d'ajouter / ou insérer des points (courbes fermées)
- pour les courbes fermées, les tests d'inclusion d'un point
- déplacer le point sélectionné au clavier (flèches, avec Majuscule ou
Contrôle). Nous n'avons pas mis en place la gestion de clavier, car les tPaintBox et tPanel ne reçoivent pas les événements clavier, et leur mise en place oblige à créer les événements au niveau de la tForm
- gérer un liste de points sélectionnés (Clic, et Majuscule_Clic, comme en Delphi), puis déplacer à la souris ou au clavier
- ajouter d'autres fonctions d'interpolation que Bézier (des Splines)
Au niveau programmation:
- la gestion du point sélectionné pourrait aussi être améliorée (définition de set_selection qui supprimerait le précédent etc)
- nous n'avons pas du tout essayé d'optimiser les interpolations de points. Il
conviendrait de relire Numerical Receipies...
5 - Télécharger les sources Vous pouvez télécharger: Ce .ZIP qui comprend: - le .DPR, la forme principale, les formes annexes eventuelles
- les fichiers de paramètres (le schéma et le batch de création)
- dans chaque .ZIP, toutes les librairies nécessaires à chaque projet (chaque .ZIP est autonaume)
Ces .ZIP contiennent des chemins RELATIFS. Par conséquent:
- créez un répertoire n'importe où sur votre machine
- placez le .ZIP dans ce répertoire
- dézippez et les sous-répertoires nécessaires seront créés
- compilez et exécutez
Ces .ZIP ne modifient pas votre PC (pas de changement de la Base de Registre, de DLL ou autre). Pour supprimer le projet, effacez le répertoire.
Comme d'habitude:
- nous vous remercions de nous signaler toute erreur, inexactitude ou problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections
qui en résulteront pourront aider les prochains lecteurs
- tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même
les bienvenus à jcolibri@jcolibri.com.
- plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en
cliquant "envoyer" :
- et si vous avez apprécié cet article, faites connaître notre site,
ajoutez un lien dans vos listes de liens ou citez-nous dans vos blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.
6 - L'auteur John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |