Cet article fournit le code d'une horloge analogique, programmée en Python, PyGObject et Cairo. On n'est plus ici dans le cas d'un exemple minimal : 250 lignes de code !
L'horloge est responsive, comme on dit dans le Berry, c'est-à-dire que les tailles s'adaptent aux dimensions de la fenêtre :
Elle s'intègre bien dans un dock :
Le design s'inspire de la célèbre horloge de gare des Chemins de fer Fédéraux Suisses (CFF ou SBB en allemand) créée en 1944, au moins pour les graduations et la couleur de l'aiguille des secondes :
Le modèle physique pour construire les ombres des aiguilles a été fait en quelques minutes sur un coin de table et il est assez primitif. Le résultat paraît convenable et je n'ai pas cherché plus loin.
#!/usr/bin/python # jpsb le 9 août 2024 # Une horloge analogique en Python, PyGObject et Cairo # licence : GPL 3 import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib import math import time import cairo class Clock(Gtk.Window): def __init__(self): super().__init__(title="Clock") # ~ self.resize(86, 86) self.resize(800, 800) self.connect("delete-event", Gtk.main_quit) # on crée la figure et on l'ajoute à la fenêtre self.darea = Gtk.DrawingArea() self.darea.connect("draw", self.on_draw) self.add(self.darea) # on shédule la prochaine mise à jour dans 1s GLib.timeout_add_seconds(1, self.on_timer) # fonction mise à jour du dessin def on_timer(self): self.darea.queue_draw() return True # fonction calcul distance def distance(self,x,y,xp,yp): return math.sqrt((x-xp)**2+(y-yp)**2) # fonction calcul d'angle (al-Kashi) def alpha(self,a,b,c): return math.acos((c**2+b**2-a**2)/(2*b*c)) # tracé cairo de l'horloge def on_draw(self, wid, cr): # on récupère la taille de la fenêtre w=self.get_size().width h=self.get_size().height # on place l'origine du repère au centre cr.translate(w/2, h/2) # réglages : longueurs, largeurs et couleurs r=math.sqrt((w/2)**2+(h/2)**2) l_grad_5 = 18/100 l_grad = 7/100 l_hour = 60/100 l_min = 85/100 l_sec = 95/100 w_grad_5 = r/30 w_grad = r/60 w_hour = r/11 w_minute = r/14 w_second = r/53 c_grad_5 = (.2,.2,.2) c_grad = (.3,.3,.3) c_hour = (.2,.2,.2) c_minute = (.2,.2,.2) c_second = (.9,.0,.1) c_shadow = (.82,.82,.82) # coordonnées lumière pour les ombres x_a=-w/2 y_a=-h/2 theta = math.atan(x_a/y_a) # hauteur des aiguilles k=.04*r # fond en dégradé cr.rectangle(-w/2, -h/2, w, h) r1 = cairo.RadialGradient(0.1*r, 0.05*r, 0.2*r, 0.05*r, 0.1*r, r) r1.add_color_stop_rgb(0, 1, 1, 1) r1.add_color_stop_rgb(1, .8, .8, .8) cr.set_source(r1) cr.fill() # recup de l'heure now = time.localtime() hour = now.tm_hour % 12 minute = now.tm_min second = now.tm_sec # calculs aiguille des heures et ombre hour_angle = (hour + minute/60) * math.pi/6 - math.pi/2 hour_end_x = l_hour * w/2 * math.cos(hour_angle) hour_end_y = l_hour * h/2 * math.sin(hour_angle) hour_start_x = -0.15 * w/2 * math.cos(hour_angle) hour_start_y = -0.15 * h/2 * math.sin(hour_angle) ap=self.distance(0,0,hour_start_x,hour_start_y) op=self.distance(x_a,y_a,hour_start_x,hour_start_y) sp=self.distance(0,0,x_a,y_a) alpha_anglep=self.alpha(ap,sp,op) hour_shadow_start_x = hour_start_x + k*math.tan(theta-alpha_anglep) hour_shadow_start_y = hour_start_y + k a=self.distance(0,0,hour_end_x,hour_end_y) o=self.distance(x_a,y_a,hour_end_x,hour_end_y) e=self.distance(0,0,x_a,y_a) alpha_angle=self.alpha(a,o,e) hour_shadow_end_x = hour_end_x + k*math.tan(theta-alpha_angle) hour_shadow_end_y = hour_end_y + k # calculs aiguilles des minutes et ombre minute_angle = (minute + second/60) * math.pi/30 - math.pi/2 minute_end_x = l_min * w/2 * math.cos(minute_angle) minute_end_y = + l_min * h/2 * math.sin(minute_angle) minute_start_x = -0.20 * w/2 * math.cos(minute_angle) minute_start_y = -0.20 * h/2 * math.sin(minute_angle) ap=self.distance(0,0,minute_start_x,minute_start_y) op=self.distance(x_a,y_a,minute_start_x,minute_start_y) sp=self.distance(0,0,x_a,y_a) alpha_anglep=self.alpha(ap,sp,op) minute_shadow_start_x = minute_start_x + k*math.tan(theta-alpha_anglep) minute_shadow_start_y = minute_start_y + k a=self.distance(0,0,minute_end_x,minute_end_y) o=self.distance(x_a,y_a,minute_end_x,minute_end_y) e=self.distance(0,0,x_a,y_a) alpha_angle=self.alpha(a,o,e) minute_shadow_end_x = minute_end_x + k*math.tan(theta-alpha_angle) minute_shadow_end_y = minute_end_y + k # calculs aiguilles des secondes et ombre second_angle = second * math.pi/30 - math.pi/2 second_end_x = l_sec * w/2 * math.cos(second_angle) second_end_y = l_sec * h/2 * math.sin(second_angle) second_start_x = -0.28 * w/2 * math.cos(second_angle) second_start_y = -0.28 * h/2 * math.sin(second_angle) ap=self.distance(0,0,second_start_x,second_start_y) op=self.distance(x_a,y_a,second_start_x,second_start_y) sp=self.distance(0,0,x_a,y_a) alpha_anglep=self.alpha(ap,sp,op) second_shadow_start_x = second_start_x + k*math.tan(theta-alpha_anglep) second_shadow_start_y = second_start_y + k a=self.distance(0,0,second_end_x,second_end_y) o=self.distance(x_a,y_a,second_end_x,second_end_y) e=self.distance(0,0,x_a,y_a) alpha_angle=self.alpha(a,o,e) second_shadow_end_x = second_end_x + k*math.tan(theta-alpha_angle) second_shadow_end_y = second_end_y + k # tracé des ombres cr.set_line_cap(cairo.LINE_CAP_ROUND) cr.set_source_rgb(*c_shadow) cr.move_to(hour_shadow_start_x, hour_shadow_start_y) cr.line_to(hour_shadow_end_x, hour_shadow_end_y) cr.set_line_width(w_hour*(1+k/y_a)) cr.stroke() cr.set_source_rgb(*c_shadow) cr.move_to(minute_shadow_start_x, minute_shadow_start_y) cr.line_to(minute_shadow_end_x, minute_shadow_end_y) cr.set_line_width(w_minute*(1+k/y_a)) cr.stroke() cr.set_source_rgb(*c_shadow) cr.move_to(second_shadow_start_x, second_shadow_start_y) cr.line_to(second_shadow_end_x, second_shadow_end_y) cr.set_line_width(w_second*(1+k/y_a)) cr.stroke() # tracé des graduations cr.set_line_cap(cairo.LINE_CAP_BUTT) for i in range(60): angle = i * math.pi/30 - math.pi/2 x2 = 1 * w/2 * math.cos(angle) y2 = 1 * h/2 * math.sin(angle) if i % 5 == 0: x1 = (1-l_grad_5) * w/2 * math.cos(angle) y1 = (1-l_grad_5) * h/2 * math.sin(angle) cr.set_source_rgb(*c_grad_5) cr.move_to(x1, y1) cr.line_to(x2, y2) cr.set_line_width(w_grad_5) cr.stroke() else: x1 = (1-l_grad) * w/2 * math.cos(angle) y1 = (1-l_grad) * h/2 * math.sin(angle) cr.set_source_rgb(*c_grad) cr.move_to(x1, y1) cr.line_to(x2, y2) cr.set_line_width(w_grad) cr.stroke() # tracé des aiguilles cr.set_line_cap(cairo.LINE_CAP_ROUND) cr.set_source_rgb(*c_minute) cr.move_to(hour_start_x, hour_start_y) cr.line_to(hour_end_x, hour_end_y) cr.set_line_width(w_hour) cr.stroke() cr.set_source_rgb(*c_minute) cr.move_to(minute_start_x, minute_start_y) cr.line_to(minute_end_x, minute_end_y) cr.set_line_width(w_minute) cr.stroke() cr.set_source_rgb(*c_second) cr.move_to(second_start_x, second_start_y) cr.line_to(second_end_x, second_end_y) cr.set_line_width(w_second) cr.stroke() # tracé petit disque rouge au centre des aiguilles cr.arc(0, 0, r/40, 0, 2*math.pi) cr.fill() cr.stroke() # tracé filet autour de la fenêtre cr.set_source_rgb(0.8,0.8,0.8) cr.move_to(-w/2, +h/2) cr.line_to(-w/2, -h/2) cr.line_to(w/2, -h/2) cr.set_line_width(2) cr.stroke() cr.set_source_rgb(0.2,0.2,0.2) cr.move_to(-w/2, +h/2) cr.line_to(+w/2, +h/2) cr.line_to(+w/2, -h/2) cr.set_line_width(1) cr.stroke() # ============================================================ # boucle principale win = Clock() win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main()
C'est le quatrième article consacré à la programmation Python, GTK et PyGObject. Les trois premiers :
Articles (304)
Je préfère vraiment les contacts à l'ancienne, par courrier électronique à l’adresse jpsmail(at)free.fr. Antispam : penseras-tu à remplacer (at) par @ dans l’adresse ? Que cela ne t'enpêche pas d'ajouter un commentaire :