pátek 30. srpna 2013

GTK+3 - průhledné widgety, barvy, souřadnice myši, ..

Před časem jsem začal psát o využívání GTK+ toolkitu při výrobě oken pro své aplikace a rád bych na to navázal. Klasické "Hello World!" ukázky jsou neskutečně nudné a proto budu pokračovat tím, co zajímá každého začátečníka: průhlednost, barvičky a jiné efektní kraviny, prostě zábava. Výsledkem bude okénko s textem, kterému je možné měnit barvy popředí i pozadí včetně průhlednosti, kolečkem myši měnit velikost a na požádání se bude schovávat, když se nad ním zdrží myška, tedy i nějaké to dolování souřadnic a rozměrů jak okna, tak kurzoru myši. Jo a MRDEL je normální slušné slovo! ...



Python jsem sice prozatím opustil a příklady budu psát ve Vala, ale co tu máme PyGObject a wrapper založený na introspekci, není to v Pythonu až tak jiné, jako to bylo s PyGTK. I když některé zásadnější rozdíly v přístupu, dané syntaxí jazyka, tu samozřejmě jsou. V komentářích kódu některé zmíním. Každopádně pokud by si někdo nevěděl rady ať už v Pythonem, nebo Vala, může se ozvat, co vím, to napíšu.

Výtvor se skládá pouze z okna bez dekorace, na kterém je jeden řádek textu. Okno je ve výchozím stavu úplně průhledné a text zelený a neprůhledný, ale barvu, i průhlednost obou částí je možné měnit přes kontextové menu. Text si zvolíte při spuštění programu, dáte ho na místo prvního (a jediného) parametru a z kontextového menu můžete měnit i to, zda se bude zobrazovat tučně nebo normálně. Levým myšítkem můžete widget přemisťovat po pracovní ploše a otáčením kolečka myši je možné měnit velikost písma a tím i celého okna. Nabídka kontextového menu zahrnuje ještě funkci automatického schovávání, pokud se kurzor myši zdrží nad widgetem, která prosviští časovače a shánění souřadnic jak okna, tak myšítka.

Barvy a průhlednost widgetů

Průhlednost okna jsem původně (už v GTK2) řešil přes knihovnu Cairo, protože jsem tehdy jiné řešení nenašel. GTK3 lze ale barvu i s průhledností cpát do widgetů přímo, jen je třeba změnit model vizualizace, aby fungoval ve 32 bitech, což není výchozí stav ani v dnešních kompozitních prostředích. Vypadá to zhruba takto:

widget.set_visual ( widget.get_screen().get_rgba_visual() );
widget.override_color (Gtk.StateFlags.NORMAL, fg_color);
widget.override_background_color (Gtk.StateFlags.NORMAL, bg_color);

přičemž xg_color musí být objekty typu Gdk.RGBA. Je také třeba mít na paměti, že takto se barví popředí a pozadí jednotlivých widgetů, nestčí tedy přerazit barvy pozadí a popředí okna a očekávat, že to budou dědit všechny widgety, které do něj přihodíte. Ovšem tyto úpravy se hodí jen pro speciální využití, rozhodně ne na barvení celých aplikací, takový přístup je leda pro widláky.

Pokud by vás zajímala metoda s Cairem, které vám umožní další legrácky, nejjednodušší varianta překreslení pozadí widgetu vlastní barvou je následující:

.....
        // Je třeba povolit kreslení, ne všechny widgety to ale akceptují
        widget.set_app_paintable (true);
        // Pokaždé, když se okno bude mít překreslit, zavolá se metoda draw_widget
        widget.draw.connect (draw_widget);
        widget.set_visual ( widget.get_screen().get_rgba_visual() );


    private bool draw_widget (Gtk.Widget w, Cairo.Context cr) {
        // nejprve je třeba obsah widgetu smazat
        cr.set_operator(Cairo.Operator.CLEAR);
        var w_width = w.get_allocated_width ();
        var w_height = w.get_allocated_height ();
        cr.rectangle (0.0, 0.0, w_width, w_height);
        cr.fill ();
        // a pak ho překreslit barvou podle přání
        cr.set_operator(Cairo.Operator.OVER);
        cr.rectangle (0.0, 0.0, w_width, w_height);
        cr.set_source_rgba ( red, green, blue, alpha );
        cr.fill ();
        // metoda musí vracet false, aby signál prošel dál a Gtk dokreslilo zbytek okna
        return false;

Barvy bere cairo stejně jako Gdk.RGBA, tedy každá je typu double v rozsahu 0 až 1. Jistě jste si také všimli, že si metoda draw_widget zjišťuje rozměry svého svěřence aby mohla vše pěkně počmárat a tudíž, že k výrobě pěkného rámečku stačí málo.

Ostatní

Jinak jsem použil jen aktuální komponenty GTK3 a zastaralým se schválně vyhnul, byť třeba starý dialog pro výběr barvy mám raději.

GTK3 Dialog pro výběr barvy

Jedním z problémů, které jsem zatím nevyřešil bezezbytku, je získání reference na zařízení (typu Gdk.Device) obsluhující myš, ze kterého čtu souřadnice kurzoru na obrazovce. Měl by být odkazovaný ve všech eventech, které zařízení vyvolá, ovšem z entry-notify-event, který by byl ideální, ho dostat neumím, přímo obsažen není. Proto ho beru až z kliknutí, což je pro danou konfiguraci programu ok, protože funkci schovávání okna je třeba z menu zapnout, ale pokud by byla výchozím chováním, došlo by k chybě, protože proměnná odkazující na myšáka by byla prázdná, dokud by se do okna nekliklo. Původně jsem tento problém řešil přes Gdk.Display (v kódu je použití v komentáři) ten dostanete vždy, jenže potřebné metody jsou již značené jako zastaralé a pomalu vyhynou úplně. Smysl ten přesun dává, jen mi zatím není jasné, proč mi dokumentace naznačuje, že z Gdk.EventCrossing dostanu referenci na Gdk.Device, které je původcem události a já toho nejsem schopen. Máte-li řešení, sem s ním.

Každopádně zbytek je v komentářích přímo v kódu widget.vala:

// 2013 GdH - widget.vala - example
// sudo apt-get install gcc valac libgtk-3-dev
// kompilace:
// valac --pkg gtk+-3.0 widget.vala

using Gtk;

public class Widget : Gtk.Window
{
    public Gtk.Label label;
    private Style w_style;
    private Gtk.Menu menu;
    private uint over_timeout;
    private ulong mouse_enter_signal;
    private ulong mouse_leave_signal;
    private Gtk.CheckMenuItem hide_on_mouse_item;
    private Gdk.Device pointer_device;

    public Widget (string[] args)
    {
        // Protože základní vizualizace GTK widgetů neposlouchá alfa kanál,
        // tedy nastavení průhlednosti, je třeba ji vyměnit
        // Samozřejmě je podmínkou fungující kompozitní správce oken, aby to mělo smysl,
        // což je nejen v Ubuntu již přes šest let standard
        this.set_visual ( this.get_screen().get_rgba_visual() );

        this.set_decorated (false); // Dekorace netřeba,
        this.set_resizable (false); // ručně měnit velikost taktéž,
        this.stick (); // okno bude na všech desktopech
        this.set_skip_taskbar_hint (true); // a nebude s zobrazovat v seznamu oken,
        this.set_keep_above (true); // bude vždy nahoře,
        this.accept_focus = false; // a nikdy žádnému jinému oknu nesebere fokus

        // Widget bude reagovat na tlačítka myši a na otáčení kolečka
        // což je třeba povolit patřičnou maskou:
        this.add_events (Gdk.EventMask.BUTTON_PRESS_MASK |
                         Gdk.EventMask.SCROLL_MASK);
        // a připojit
        this.button_press_event.connect (clicked);
        this.scroll_event.connect (change_size);

        // label bude zobrazovat text, který programu předáte jako první argument při spouštění
        label = new Gtk.Label (args[1]);
        w_style = new Style ();
        label.set_margin_left (3);
        label.set_margin_right (3);
        label.show ();
        this.add (label);
        update_style ();
        build_menu ();
    }

    private bool clicked (Gdk.EventButton event)
    {
        // Primárním (levým) tlačítkem bude možné
        // okno přemisťovat
        if (event.button == 1)
            this.begin_move_drag ((int)event.button,
                                  (int)event.x_root,
                                  (int)event.y_root,
                                  event.time);
        // zatímco sekundární (pravé) tlačítko vyvolá menu
        else if (event.button == 3)
            this.menu.popup (null, null, null, event.button, event.time);
        // Následující příkaz získá referenci na zařízení,
        // které kliknutí provedlo, tedy myš nejčastěji,
        // aby se z něj později dala zjišťovat pozice kurzoru
        // Lepší by bylo použít enter-notify-event,
        // ale z něj to vydolovat neumím, nepředává ho přímo
        if (pointer_device != event.device)
            pointer_device = event.device;
        return true;
    }
 
    private bool change_size (Gdk.EventScroll event)
    {
        // velikost widgetu se přizpůsobuje velikosti písma,
        // které je možné měnit otáčením kolečka myši nad widgetem
        // zvětšování i zmenšování probíhá po 10% z aktuální velikosti
        if (event.direction == Gdk.ScrollDirection.UP) {
            w_style.size += w_style.size/10;
            if (w_style.size > 200) w_style.size = 200;
        }
        else if (event.direction == Gdk.ScrollDirection.DOWN) {
            w_style.size -= w_style.size/10;
            if (w_style.size < 10) w_style.size = 10;
        }
        update_style ();
        return true;
    }

    private void update_style ()
    {
        // Aktualizace vzhledu widgetu podle parametrů uložených v třídě "Style"
        // Nastavení fontu
        label.override_font (Pango.FontDescription.from_string
            ("%s %s %d".printf (w_style.font, w_style.bold, w_style.size)));
        // Barva písma
        label.override_color (Gtk.StateFlags.NORMAL, w_style.fg_color);
        // Barva pozadí okna
        this.override_background_color (Gtk.StateFlags.NORMAL, w_style.bg_color);
    }

    private bool on_mouse_enter ()
    {
        // Je-li aktivní funkce schovávání widgetu pod myší,
        // způsobí vstup kurzoru myši nad widget volání této metody.
        // Pokud by se widget schoval okamžitě, nedal by se dál ovládat,
        // proto 600ms timeout
        over_timeout = GLib.Timeout.add (600, hide_widget);
        return true;
    }

    private bool on_mouse_leave ()
    {
        // Pokud myš opustí widget do času definovaného časovačem over_timeout,
        // tento časovač se zruší a ke schování nedojde
        GLib.Source.remove (over_timeout);
        return true;
    }

    private bool hide_widget ()
    {
        // Myš neopustila widget v definovaném čase a tak se zavolala tato metoda,
        // která widget schová, aby nepřekážel
        int wx;
        int wy;
        // Zjistíme si polohu a rozměry widgetu
        // Zde je třeba zásadní rozdíl oproti použití z Pythonu:
        // zatímco z C/Vala se volá void metoda, která naplní předané proměnné,
        // z Pythonu tato metoda vrací seznam.
        // Použití z Pythonu by tedy vypadalo následovně:
        // n, wx, wy = self.get_window().get origin()
        // PyGObject vrací o jeden parametr navíc (n), (už) ani nevím k čemu je dobrý,
        // je to specialita PyGObject wrapperu, protože originál toho víc nedá
        this.get_window().get_origin(out wx, out wy);
        Gtk.Allocation alloc;
        this.get_allocation (out alloc);
        int w = alloc.width;
        int h = alloc.height;
        this.hide ();
        // widget se schová a spustí se další časovač, který bude každou sekundu
        // volat metodu, jenž bude zjišťovat, zda je myš stále v oblasti widgetu
        // a případně ho opět zobrazí
        GLib.Timeout.add_seconds (1, () =>
                // Metoda "show_widget" vrací true, pokud je widget stále schovaný,
                // false pokud ho již zobrazila
                // Vrátí-li se false, zruší se současně i tento časovač,
                // kterému je výsledek předán
                { return show_widget (wx, wy, w, h); }
            );
        // vrácením false se zruší již nepotřebný časovač "over_timeout"
        return false;
    }

    private bool show_widget (int wx, int wy, int w, int h)
    {
        // Po schování widgetu se každou sekundu spustí tato metoda,
        // která si podle předaných parametů zkontroluje, zda je již myšák pryč
        // a případně widget opět zobrazí
        int px;
        int py;
        Gdk.Screen scr;
        // Zastaralá (ale stále funkční) metoda zjištění pozice myši:
        // Gdk.ModifierType mask;
        // var dis = Gdk.Display.get_default();
        // dis.get_pointer(out scr, out px, out py, out mask);

        // Nově se má zjišťovat z konkrétního zařízení,
        // jenže to je třeba nejprve "sehnat"
        // konkrétní objekt odvozený od Gdk.Device
        // by měl být toreticky dostupný v každém eventu,
        // který konkrétní zařízení vyvolá, ale z některých to nějak neumím dostat
        pointer_device.get_position(out scr, out px, out py);
        // Pokud budou souřadnice ukazatele myši mimo widget, vrátí se true
        // Tohle řetězení porovnání je zatím ve Vala jen experimentální,
        // tudíž se na to nedá spoléhat stoprocentně,
        // ale problémy jsem v tomto případě nezaznamenal
        if ((wx < px < (wx+w)) & (wy < py < (wy+h))) return true;
        else {
            this.show ();
            // po zobrazení okna je ho třeba vrátit na původní pozici,
            // správce oken ho flákne, kam uzná za vhodné
            this.move (wx, wy); 
        }
        return false;
    }

    private void toggle_hide_on_mouse_over ()
    {
        // Změna zaškrtávátka pro schování widgetu po najetí myši nad něj
        // jednoduše připojí, nebo odpojí signály, které tuto reakci vyvolávají
        if (hide_on_mouse_item.get_active()) {
            mouse_enter_signal = this.enter_notify_event.connect (on_mouse_enter);
            mouse_leave_signal = this.leave_notify_event.connect (on_mouse_leave);
        }
        else {
            this.disconnect (mouse_enter_signal);
            this.disconnect (mouse_leave_signal);
        }
    }

    private void build_menu ()
    {
        menu = new Gtk.Menu ();
        // Aktivace následujících dvou položek menu
        // zavolá metodu s volbou barvy a předá referenci na barvu, která se má měnit.
        // Bez "ref" by byla předána jen kopie, neboť jde o objekt typu struct
        var item = new Gtk.MenuItem.with_mnemonic ("_Foreground Color");
        item.activate.connect (() => {change_color (ref w_style.fg_color);});
        menu.add(item);

        item = new Gtk.MenuItem.with_mnemonic ("Back_ground Color");
        item.activate.connect (() => {change_color (ref w_style.bg_color);});
        menu.add(item);

        hide_on_mouse_item = new Gtk.CheckMenuItem.with_mnemonic ("_Hide on Mouse Over");
        hide_on_mouse_item.activate.connect (toggle_hide_on_mouse_over);
        menu.add(hide_on_mouse_item);

        var chitem = new Gtk.CheckMenuItem.with_mnemonic ("_Bold");
        // lambda funkce jsou prostě fajn, tohle se ještě snese :)
        chitem.activate.connect (() => { if (chitem.get_active ())
                                        w_style.bold = "bold";
                                        else w_style.bold = "";
                                        update_style ();
                                        });
        chitem.activate (); // rovnou na to kliknem, výchozí bude tučný font
        menu.add (chitem);

        item = new Gtk.SeparatorMenuItem ();
        menu.add (item);

        item = new Gtk.MenuItem.with_mnemonic ("_Quit");
        item.activate.connect (Gtk.main_quit);
        menu.add (item);

        menu.show_all ();
    }

    private void change_color (ref Gdk.RGBA color)
    {
        // Jako "color" je předávána reference a tak je ji třeba označit i na příjmu
        // Na výrobu dialogu s výběrem barvy stačí jeden příkaz:
        var cs = new Gtk.ColorChooserDialog("Select color", null);
        // Výchozí barvou bude ta aktuální:
        cs.set_rgba (color);
        // Metoda run zobrazí dialog a zastaví zbytek aplikace
        // dokud uživatel nestiskne tlačítko, které vrací na výstupu
        if (cs.run () == Gtk.ResponseType.OK) {
            color = cs.get_rgba ();
            update_style ();
        }
        // dialog je po použití třeba zlikvidovat:
        cs.destroy ();
    }

}

// V této třídě se uchovávají vlastnosti widgetu
public class Style
{
    public Gdk.RGBA fg_color;
    public Gdk.RGBA bg_color;
    public int size;
    public string bold;
    public string font;
    public Style () {
        fg_color = Gdk.RGBA ();
            fg_color.red = 0.0;
            fg_color.green = 1.0;
            fg_color.blue = 0.0;
            fg_color.alpha = 1.0;
        bg_color = Gdk.RGBA ();
            bg_color.red = 0.0;
            bg_color.green = 0.0;
            bg_color.blue = 0.0;
            bg_color.alpha = 0.0;
        size = 28;
        bold = ""; // or "bold"
        font = "Sans";
    }
}

public int main (string[] args)
{
    Gtk.init(ref args);
    var widget = new Widget(args);
    widget.show_all();
    Gtk.main();
            
    return 0;
}

Žádné komentáře:

Okomentovat

Zkuste prosím při komentováni používat místo volby Anonymní volbu Název/adresa URL, kde vyplníte nějakou přezdívku, adresu zadávat nemusíte. Vědět, které příspěvky jsou od jednoho člověka, je fajn. Díky.

Pokud by se vám náhodou odeslaný komentář na stránce nezobrazil, vytáhnu ho z koše hned jak si toho všimnu. I Google spam filter se občas sekne.