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