Running clock in a transparent window drawn with Cairo
Posted: Thu Aug 23, 2012 2:52 pm
This is a simple example to demonstrate how to display a running clock using Cairo functions. Cairo is built-in in all Linux distributions, so you don't have to install any additional libraries.
I didn't reinvent the wheel but converted C sourcecode snippets from a tutorial of Davyd Madely to PureBasic. The first example simply displays a running clock in a PB window:

For the second example I modified the first one to display the same opaque clock as above in a totally transparent window. You can use this technique to position widgets directly on the desktop or on other windows without any visible window. Since the window has no titlebar anymore you have to close the clock by pressing the Escape key...

I tested both examples successfully in these Linux distributions:
- andLinux/Kubuntu 9.04 (2nd example will terminate with an error message because of missing support for transparency)
- Linux Mint 13 Cinnamon
- Kubuntu 10.04 (only partial transparency of window in 2nd example)
- Kubuntu 12.04
- OpenSuSE 12.1
- Ubuntu 12.04 (Unity)
- Xubuntu 12.04
GTK+ started in 2005 with version 2.8 to use Cairo to render the majority of its widgets.Wikipedia wrote:Cairo is a software library used to provide a vector graphics-based, device-independent API for software developers. It is designed to provide primitives for 2-dimensional drawing across a number of different backends. Cairo is designed to use hardware acceleration[1] when available.
I didn't reinvent the wheel but converted C sourcecode snippets from a tutorial of Davyd Madely to PureBasic. The first example simply displays a running clock in a PB window:

Code: Select all
; Converted from C sourcecode in Davyd Madeley's tutorial:
; http://www.gnomejournal.org/article/34/writing-a-widget-using-cairo-and-gtk28
; http://gnomejournal.org/article/36/writing-a-widget-using-cairo-and-gtk28-part-2
EnableExplicit
ImportC ""
gdk_cairo_create(*Drawable.GdkDrawable)
gdk_window_set_composited(*Window.GdkWindowObject, Composited.I)
EndImport
ImportC "-lcairo"
cairo_arc(*CairoContext, xCenter.D, yCenter.D, Radius.D, StartAngle.D, EndAngle.D)
cairo_destroy(*CairoContext)
cairo_fill_preserve(*CairoContext)
cairo_get_line_width(*CairoContext)
cairo_line_to(*CairoContext, x.D, y.D)
cairo_move_to(*CairoContext, x.D, y.D)
cairo_restore(*CairoContext)
cairo_save(*CairoContext)
cairo_set_line_width(*CairoContext, LineWidth.D)
cairo_set_source_rgb(*CairoContext, Red.D, Green.D, Blue.D)
cairo_stroke(*CairoContext)
EndImport
ProcedureC DrawClockCallback(*Widget.GtkWidget, *Event.GdkEventExpose)
Protected CairoContext.I
Protected Hours.I
Protected i.I
Protected Inset.I
Protected Minutes.I
Protected Radius.D
Protected Seconds.I
Protected xCenter.D
Protected yCenter.D
Hours = Hour(Date())
Minutes = Minute(Date())
Seconds = Second(Date())
; ----- Draw clock face
xCenter = *Widget\allocation\width / 2
yCenter = *Widget\allocation\height / 2
If xCenter < yCenter
Radius = *Widget\allocation\width / 2 - 5
Else
Radius = *Widget\allocation\height / 2 - 5
EndIf
CairoContext = gdk_cairo_create(*Widget\window)
cairo_arc(CairoContext, xCenter, yCenter, Radius, 0, 2 * #PI)
cairo_set_source_rgb(CairoContext, 1, 1, 1)
cairo_fill_preserve(CairoContext)
cairo_set_source_rgb(CairoContext, 0, 0, 0)
cairo_stroke(CairoContext)
For i = 0 To 11
If i % 3 = 0
Inset = 0.2 * Radius
Else
Inset = 0.1 * Radius
EndIf
cairo_move_to(CairoContext, xCenter + (Radius - Inset) * Cos(i * #PI / 6), yCenter + (Radius - Inset) * Sin(i * #PI / 6))
cairo_line_to(CairoContext, xCenter + Radius * Cos(i * #PI / 6), yCenter + Radius * Sin(i * #PI / 6))
cairo_stroke(CairoContext)
Next i
cairo_save(CairoContext)
; ----- Draw hour hand
; The hour hand is rotated 30 degrees (Pi/6r) per hour + 1/2 a Degree
; (Pi/360) per minute
; cairo_set_line_width(CairoContext, 2.5 * cairo_get_line_width(CairoContext))
cairo_set_line_width(CairoContext, 5)
cairo_move_to(CairoContext, xCenter, yCenter)
cairo_line_to(CairoContext, xCenter + Radius / 2 * Sin(#PI / 6 * Hours + #PI / 360 * Minutes), yCenter + Radius / 2 * -Cos(#PI / 6 * Hours + #PI / 360 * Minutes))
cairo_stroke(CairoContext)
; ----- Draw minute hand
; The minute hand is rotated 6 degrees (Pi/30r) per minute
cairo_set_line_width(CairoContext, 2)
cairo_move_to (CairoContext, xCenter, yCenter)
cairo_line_to (CairoContext, xCenter + Radius * 0.75 * Sin(#PI / 30 * Minutes), yCenter + Radius * 0.75 * -Cos(#PI / 30 * Minutes))
cairo_stroke (CairoContext)
; ----- Draw seconds hand
; Operates identically to the minute hand
cairo_save(CairoContext)
cairo_set_source_rgb(CairoContext, 1, 0, 0)
cairo_move_to (CairoContext, xCenter, yCenter)
cairo_line_to (CairoContext, xCenter + Radius * 0.7 * Sin(#PI / 30 * Seconds), yCenter + Radius * 0.7 * -Cos(#PI / 30 * Seconds))
cairo_stroke (CairoContext)
cairo_restore(CairoContext)
cairo_destroy(CairoContext)
EndProcedure
Define FixedBox.I
Define Region.I
Define *Window.GtkWidget
*Window = OpenWindow(0, 140, 100, 200, 200, "Clock", #PB_Window_SizeGadget)
FixedBox = g_list_nth_data_(gtk_container_get_children_(gtk_bin_get_child_(WindowID(0))), 0)
g_signal_connect_(FixedBox, "expose-event", @DrawClockCallback(), 0)
AddWindowTimer(0, 0, 1000)
Repeat
Select WaitWindowEvent()
Case #PB_Event_CloseWindow
RemoveWindowTimer(0, 0)
Break
Case #PB_Event_Timer
Region = gdk_drawable_get_clip_region_(*Window\window)
gdk_window_invalidate_region_(*Window\window, Region, #True)
gdk_window_process_updates_(*Window\window, #True)
gdk_region_destroy_(Region)
EndSelect
ForEver

I tested both examples successfully in these Linux distributions:
- andLinux/Kubuntu 9.04 (2nd example will terminate with an error message because of missing support for transparency)
- Linux Mint 13 Cinnamon
- Kubuntu 10.04 (only partial transparency of window in 2nd example)
- Kubuntu 12.04
- OpenSuSE 12.1
- Ubuntu 12.04 (Unity)
- Xubuntu 12.04
Code: Select all
; Converted from C sourcecode in Davyd Madeley's tutorial:
; http://www.gnomejournal.org/article/34/writing-a-widget-using-cairo-and-gtk28
; http://gnomejournal.org/article/36/writing-a-widget-using-cairo-and-gtk28-part-2
EnableExplicit
#CAIRO_OPERATOR_SOURCE = 1
ImportC ""
gdk_cairo_create(*Drawable.GdkDrawable)
gdk_screen_get_rgba_colormap(*Screen.GdkScreen)
EndImport
ImportC "-lcairo"
cairo_arc(*CairoContext, xCenter.D, yCenter.D, Radius.D, StartAngle.D, EndAngle.D)
cairo_destroy(*CairoContext)
cairo_fill_preserve(*CairoContext)
cairo_line_to(*CairoContext, x.D, y.D)
cairo_move_to(*CairoContext, x.D, y.D)
cairo_paint(CairoContext)
cairo_restore(*CairoContext)
cairo_save(*CairoContext)
cairo_set_line_width(*CairoContext, LineWidth.D)
cairo_set_operator(*CairoRegion, CairoOperator.I)
cairo_set_source_rgba(*CairoRegion, Red.D, Green.D, Blue.D, Alpha.D)
cairo_stroke(*CairoContext)
EndImport
ProcedureC DrawClockCallback(*Widget.GtkWidget, *Event.GdkEventExpose)
Protected CairoContext.I
Protected Hours.I
Protected i.I
Protected Inset.I
Protected Minutes.I
Protected Radius.D
Protected Seconds.I
Protected xCenter.D
Protected yCenter.D
; ----- Draw transparent background
CairoContext = gdk_cairo_create(*Widget\window)
cairo_set_source_rgba(CairoContext, 1, 1, 1, 0)
cairo_set_operator(CairoContext, #CAIRO_OPERATOR_SOURCE)
cairo_paint(CairoContext)
; ----- Update current time
Hours = Hour(Date())
Minutes = Minute(Date())
Seconds = Second(Date())
; ----- Draw clock face
xCenter = *Widget\allocation\width / 2
yCenter = *Widget\allocation\height / 2
If xCenter < yCenter
Radius = *Widget\allocation\width / 2 - 5
Else
Radius = *Widget\allocation\height / 2 - 5
EndIf
cairo_arc(CairoContext, xCenter, yCenter, Radius, 0, 2 * #PI)
cairo_set_source_rgba(CairoContext, 1, 1, 1, 1)
cairo_fill_preserve(CairoContext)
cairo_set_source_rgba(CairoContext, 0, 0, 0, 1)
cairo_stroke(CairoContext)
For i = 0 To 11
If i % 3 = 0
Inset = 0.2 * Radius
Else
Inset = 0.1 * Radius
EndIf
cairo_move_to(CairoContext, xCenter + (Radius - Inset) * Cos(i * #PI / 6), yCenter + (Radius - Inset) * Sin(i * #PI / 6))
cairo_line_to(CairoContext, xCenter + Radius * Cos(i * #PI / 6), yCenter + Radius * Sin(i * #PI / 6))
cairo_stroke(CairoContext)
Next i
cairo_save(CairoContext)
; ----- Draw hour hand
; The hour hand is rotated 30 degrees (Pi/6r) per hour + 1/2 a Degree
; (Pi/360) per minute
cairo_set_line_width(CairoContext, 5)
cairo_move_to(CairoContext, xCenter, yCenter)
cairo_line_to(CairoContext, xCenter + Radius / 2 * Sin(#PI / 6 * Hours + #PI / 360 * Minutes), yCenter + Radius / 2 * -Cos(#PI / 6 * Hours + #PI / 360 * Minutes))
cairo_stroke(CairoContext)
; ----- Draw minute hand
; The minute hand is rotated 6 degrees (Pi/30r) per minute
cairo_set_line_width(CairoContext, 2)
cairo_move_to(CairoContext, xCenter, yCenter)
cairo_line_to(CairoContext, xCenter + Radius * 0.75 * Sin(#PI / 30 * Minutes), yCenter + Radius * 0.75 * -Cos(#PI / 30 * Minutes))
cairo_stroke(CairoContext)
; ----- Draw seconds hand
; Operates identically to the minute hand
cairo_save(CairoContext)
cairo_set_source_rgba(CairoContext, 1, 0, 0, 1)
cairo_move_to(CairoContext, xCenter, yCenter)
cairo_line_to(CairoContext, xCenter + Radius * 0.7 * Sin(#PI / 30 * Seconds), yCenter + Radius * 0.7 * -Cos(#PI / 30 * Seconds))
cairo_stroke(CairoContext)
cairo_restore(CairoContext)
cairo_destroy(CairoContext)
EndProcedure
Procedure RedrawWidget(*Widget.GtkWidget)
Protected CairoContext.I
CairoContext = gdk_cairo_create(*Widget\window)
cairo_set_source_rgba(CairoContext, 1.0, 1.0, 1.0, 0.5)
cairo_set_operator(CairoContext, #CAIRO_OPERATOR_SOURCE)
cairo_paint(CairoContext)
cairo_destroy(CairoContext)
EndProcedure
Define Colormap.I
Define FixedBox.I
Define Region.I
Define Screen.I
Define *Window.GtkWidget
*Window = OpenWindow(0, 140, 100, 200, 200, "Clock", #PB_Window_Invisible)
gtk_window_set_decorated_(*Window, #False)
FixedBox = g_list_nth_data_(gtk_container_get_children_(gtk_bin_get_child_(WindowID(0))), 0)
g_signal_connect_(FixedBox, "expose-event", @DrawClockCallback(), 0)
gtk_widget_set_app_paintable_(FixedBox, #True)
; ----- Delete GdkWindow resources to be able to change colormap
gtk_widget_unrealize_(*Window)
; ----- Change colormap to support alpha values and hence allow transparency
Screen = gtk_widget_get_screen_(*Window)
Colormap = gdk_screen_get_rgba_colormap(Screen)
If Colormap
gtk_widget_set_colormap_(*Window, Colormap)
Else
MessageRequester("Error", "Your current system configuration doesn't support transparency!")
End
EndIf
; ----- Realize window and rebuild new GdkWindow
gtk_widget_show_all_(*Window)
AddWindowTimer(0, 0, 1000)
AddKeyboardShortcut(0, #PB_Shortcut_Escape, 0)
Repeat
Select WaitWindowEvent()
Case #PB_Event_Menu
If EventMenu() = 0
RemoveWindowTimer(0, 0)
Break
EndIf
Case #PB_Event_Timer
Region = gdk_drawable_get_clip_region_(*Window\window)
gdk_window_invalidate_region_(*Window\window, Region, #True)
gdk_window_process_updates_(*Window\window, #True)
gdk_region_destroy_(Region)
EndSelect
ForEver