14 maggio, 2007

Python Cairo e le glass window

Chi non conosce il lavoro di njpatel? Ormai i suoi avant window navigator e affinity sono noti ai più. Le sue idee sono innovative è apprezzate da molti bloggers. In questo post non voglio solo esaltare le sue produzioni (anche perché arriverei molto in ritardo in questo) ma voglio spingermi un po oltre per spiegare il modo di operare per ottenere risultati visivi simili ma utilizzando il nostro amato pitone.

A chi non sapesse a cosa mi riferisco, rimando a questo post sul blog di njpatel. Notate le trasperenze della finestra e gli altri widget inseriti normalmente al suo interno.

Come faccio ad ottenere un risultato simile in python?


#!/usr/bin/env python

import gtk
import cairo

if gtk.pygtk_version < (2, 10, 0):
print 'Questo esempio ha bisogno delle PyGtk >= 2.10'
raise SystemExit

def hex2float(hex):
ret = []
for i in range(4):
ic = int(hex[i])
ret.append( ic / 255.0 )
return ret


class TransparentWindow(gtk.Window):
__gsignals__ = {
'expose-event': 'override',
'screen-changed': 'override',
}

def __init__(self):
gtk.Window.__init__(self)

# Indichiamo alle GTK che vogliammo disegnare noi lo sfondo.
self.set_app_paintable(True)

# non vogliamo le decorazioni del window manager
self.set_decorated(False)

# usiamo il segnale button-press-event per permettere
# il "click e trascina" sulla finestra. Ricordiamo
# che non stiamo utilizzando i classici decoratori
# del window manager e non gestendo qeusto segnale
# non sarebbe possibile spostare la finestra
self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
self.connect('button-press-event', self.on_button_press)

# Inizializziamo lo schermo
self.do_screen_changed()

def on_button_press(self, widget, event):
self.begin_move_drag(
event.button,
int(event.x_root),
int(event.y_root),
event.time)

def render_rect(self, cr, x, y, w, h, o):
# Crea un rettangolo con i bordi arrotondati
x0 = x
y0 = y
rect_width = w
rect_height = h
radius = 10 + o

x1 = x0 + rect_width
y1 = y0 + rect_height
cr.move_to(x0, y0 + radius)
cr.curve_to(x0, y0, x0, y0, x0 + radius, y0)
cr.line_to(x1 - radius, y0)
cr.curve_to(x1, y0, x1, y0, x1, y0 + radius)
cr.line_to(x1 , y1)
cr.line_to (x0 , y1)
cr.close_path()

def do_expose_event(self, event):
cr = self.window.cairo_create()

if self.supports_alpha:
cr.set_source_rgba(1.0, 1.0, 1.0, 0.0)
else:
cr.set_source_rgb(1.0, 1.0, 1.0)

cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()


(width, height) = self.get_size()
cr.move_to(0, 0)
cr.set_line_width(1.0)

cr.set_operator(cairo.OPERATOR_OVER)

pat = cairo.LinearGradient(0.0, 0.0, 0.0, height)

ex_list = [0xA1, 0xA8, 0xBB, 0xEC]
col = hex2float(ex_list)
pat.add_color_stop_rgba(0.0, col[0], col[1], col[2], col[3])

ex_list = [0x14, 0x1E, 0x3C, 0xF3]
col = hex2float(ex_list)
pat.add_color_stop_rgba(1.0, col[0], col[1], col[2], col[3])

self.render_rect(cr, 0, 0, width, height, 10)
cr.set_source(pat)
cr.fill()

# bordo luminoso
ex_list = [0xFF, 0xFF, 0xFF, 0x4e]
col = hex2float(ex_list)
cr.set_source_rgba(col[0], col[1], col[2], col[3])
self.render_rect(cr, 1.5, 1.5, width - 3 , height - 3, 10)
cr.stroke()

# bordo
ex_list = [0x00, 0x15, 0x1F, 0xe0]
col = hex2float(ex_list)
cr.set_source_rgba(col[0], col[1], col[2], col[3])
self.render_rect(cr, 0.5, 0.5, width - 1 , height - 1, 10)
cr.stroke()

ex_list = [0xFF, 0xFF, 0xFF, 0xFF]
col = hex2float(ex_list)
cr.set_source_rgba(col[0], col[1], col[2], col[3])
self.render_rect(cr, 0, 0, width , height, 10)
cr.stroke()

pat = cairo.LinearGradient(0.0, 0.0, 0.0, height)
cr.set_source(pat)
ex_list = [0xFF, 0xFF, 0xFF, 0xbb]
col = hex2float(ex_list)
pat.add_color_stop_rgba(0.0, col[0], col[1], col[2], col[3])

ex_list = [0x00, 0x00, 0x10, 0xaa]
col = hex2float(ex_list)
pat.add_color_stop_rgba(0.2, col[0], col[1], col[2], col[3])
self.render_rect(cr, 0, 0, width, 20, 10)
cr.fill()

# chiediamo esplicitamente ai figli di disegnarsi
# Se non lo facessimo non sarebbero visualizzati
# i widgets aggiunti alla TransparentWindow
children = self.get_children()
for c in children:
self.propagate_expose(c, event)

def do_screen_changed(self, old_screen=None):
screen = self.get_screen()
if self.is_composited():
print 'Il tuo schermo supporta il canale alpha!'
colormap = screen.get_rgba_colormap()
self.supports_alpha = True
else:
print 'Il tuo schermo non supporta il canale alpha! Il composite\
manager e\' attivo?'
colormap = screen.get_rgb_colormap()
self.supports_alpha = False
self.set_colormap(colormap)

class CoolAlpha:
def __init__(self):
window = TransparentWindow()
window.resize(400, 400)
eb = gtk.EventBox()
eb.set_visible_window(False)
eb.set_border_width(12)
window.add(eb)

main_box = gtk.VBox(False, 12)
eb.add(main_box)

hbox = gtk.HBox(False, 6)
main_box.pack_start(hbox, False)

button = gtk.Button()
button.set_relief(gtk.RELIEF_NONE)
button.connect("clicked", gtk.main_quit)
icon_box = gtk.HBox(False, 0)

image = gtk.Image()
image.set_from_stock(gtk.STOCK_CLOSE,
gtk.ICON_SIZE_MENU)
icon_box.pack_start(image, False)
button.add(icon_box)
hbox.pack_end(button, False)

sw = gtk.ScrolledWindow()
sw.set_property("border-width", 20)
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.textview = gtk.TextView()
self.textview.set_editable(False)
self.textview.set_cursor_visible(False)
sw.add(self.textview)
main_box.pack_start(sw, True)

window.show_all()

def set_text(self, text):
buf = self.textview.get_buffer()
buf.set_text(text)

if __name__ == '__main__':
a = CoolAlpha()
a.set_text("prova")
gtk.main()

Questo esempio è in gran parte una traduzione del codice in linguaggio c di affinity di njpatel con qualche piccolo ritocco da parte mia come l'inclusione del pulsante di chiusura in alto a destra e la textview centrale. Eseguendo l'esempio otterrete:

In genere le gtk non vengono usate per costruire finestre troppo "esotiche" e c'è un buon motivo per questo, ovvero mantenere la consistenza delle applicazioni sul desktop. Quindi nonostante queste finestre possono apparire molto belle a vedersi, il mio consiglio e di non abusarne ma utilizzarle in modo da ottenere una buona integrazione con il resto del desktop per non fare apparire la nostra applicazione come un "alieno".

7 commenti:

  1. complimenti per i post.
    mi piace molto lo stile che usi
    - che tipo di strumeti usi per postare ?
    - mi interessa molto il riquadro con lo scroll orizzontale per visualizzare il codice

    ciao Loris 8-)

    RispondiElimina
  2. Non uso alcuno strumento in particolare, posto direttamente dalla form di blogger. Se ti riferisci all'evidenziazione della sintassi uso vim per generare il codice html e per il riquadro al codice ho semplicemente modificato il foglio di stile del template blogger aggiungendo:
    .post pre {
    border: 1px solid #888888;
    padding: 8px;
    background-color:#f6f6f6;
    overflow:auto;
    }
    e utilizzando quindi il tag pre per scrivere il codice

    RispondiElimina
  3. Sembra che nelle linee 38 e 39 ci sia uno spazio in più nell'indentazione.

    Ne approfitto per complimentarmi per gli spunti che metti sul tuo blog.

    Li trovo molto stimolanti (sto imparando Python da poco :-P).

    Ciao ciao!

    RispondiElimina
  4. Il problema è che blogger a volte non mantiene i rientri come dovrebbe...

    RispondiElimina
  5. È un peccato :-(
    L'indentazione è fondamentale in Python. Dovremmo segnalarlo a Blogger (o a Google? :-D)...

    RispondiElimina
  6. Per vedere il bordo arrotondato senza parte dell sfondo occorre avere un composite manager in esecuzione?

    RispondiElimina
  7. @colossus

    mi sa di si ;)

    RispondiElimina