GTK+」タグアーカイブ

PyGObject Poppler

Web で配布されているコミックは PDF であることもある。
我がアプリで PDF も読み込みできるといいんだけど。

Poppler – Wikipedia

こんなライブラリがあることを知った。

Fedora デフォルトには普通に Gir で入っていたりするし。
そりゃ Evince が使っているし。

Poppler 0.18 (0.69.0) – Poppler 0.18

ドキュメントもある。
関数名も解りやすいし適当に書いてみよう。

#!/usr/bin/env python3

import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Poppler', '0.18')
from gi.repository import Gtk, Gio, Poppler

PDFFILE = 'りんごの色.pdf'
 
class PdfWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.connect('delete-event', Gtk.main_quit)
        # PDF file
        f = Gio.File.new_for_path(PDFFILE)
        self.pdf = Poppler.Document.new_from_gfile(f)
        self.max_page = self.pdf.get_n_pages()
        self.num = 0
        # button
        button = Gtk.Button.new_with_label('Next Page')
        button.connect('clicked', self.on_button_clicked)
        # DrawingArea
        self.canvas = Gtk.DrawingArea()
        self.canvas.connect('draw', self.on_draw)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(button, False, False, 0)
        vbox.pack_end(self.canvas, True, True, 0)
        self.add(vbox)
        self.resize(400, 400)
        self.show_all()
 
    def on_button_clicked(self, widget):
        if self.num < self.max_page-1:
            self.canvas.queue_draw()
            self.num += 1

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        page = self.pdf.get_page(self.num)
        w, h = page.get_size()
        if aw * h > ah * w:
            n = ah/h
            matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n/2, 0)
            cr.transform(matrix)
        else:
            n = aw/w
            matrix = cairo.Matrix(n, 0, 0, n, n, ah/2-h*n/2)
            cr.transform(matrix)
        page.render(cr)
 
PdfWin()
Gtk.main()

こんなにアッサリ。。。
何もインストールしないでも表示するだけならマジでこれだけ。

cairo のドキュメントを見ると PDFSurface なんてサーフェスがあるね。
こんなに面白かったのか cairo って。

Cairo Matrix

前回の続き。
Cairo.Matrix で GdkPixbuf をリサイズできるんだよね。
それなら draw シグナルのハンドラでそのまま描写すればいいジャン!

でも Matrix のリサイズは RGBA ベースじゃないだろうな?
ClutterImage ではスクリーントーンが縮小で潰れたんだよね。
screentone 2D and 3D | PaePoi

OpenGL ではないから大丈夫だと思う、ただリサイズは超ヌルヌルだった。
速度といっしょにそんな所も見ていこう。

#!/usr/bin/env python3

import gi, sys, cairo, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf1 = GdkPixbuf.Pixbuf.new_from_file('003.png')
        self.pixbuf2 = GdkPixbuf.Pixbuf.new_from_file('004.png')
        self.resize(1600, 900)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # __do__
        t = time.time()
        # Save Matrix
        cr.save()
        # Right Page
        w = self.pixbuf1.get_width()
        h = self.pixbuf1.get_height()
        n = ah/h
        matrix = cairo.Matrix(n, 0, 0, n, aw/2, n)
        cr.transform(matrix) # no! cr.set_matrix(matrix)
        Gdk.cairo_set_source_pixbuf(cr, self.pixbuf1, 0, 0)
        cr.paint()
        # Reset
        cr.restore()
        # Left Page
        w = self.pixbuf2.get_width()
        h = self.pixbuf2.get_height()
        n = ah/h
        matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n, n)
        cr.transform(matrix)
        Gdk.cairo_set_source_pixbuf(cr, self.pixbuf2, 0, 0)
        cr.paint()
        # __done__
        print('matrix  : {}'.format(time.time() - t))

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

Matrix 版

#!/usr/bin/env python3

import gi, sys, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf1 = GdkPixbuf.Pixbuf.new_from_file('003.png')
        self.pixbuf2 = GdkPixbuf.Pixbuf.new_from_file('004.png')
        self.resize(1600, 900)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # __do__
        t = time.time()
        # Right Page
        w = self.pixbuf1.get_width()
        h = self.pixbuf1.get_height()
        n = ah*w/h
        buf = self.pixbuf1.scale_simple(n, ah, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, buf, aw/2, 0)
        cr.paint()
        # Left Page
        w = self.pixbuf2.get_width()
        h = self.pixbuf2.get_height()
        n = ah*w/h
        buf = self.pixbuf2.scale_simple(n, ah, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, buf, aw/2-n, 0)
        cr.paint()
        # __done__
        print('scale:  {}'.format(time.time() - t))

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

scale_simple 版

差がなかった。
小さく描写だと scale_simple、大きく描写だと Matrix のほうがチビッと速い。
とはいえ人間の目で解るような違いは何をやっても出ない。

画質も違いが解らないレベル。
ただリサイズのスムースさは Matrix 版のほうが気のせいレベルで上かも。

cr.set_matrix(matrix) はしちゃだめよ。
原点がウインドウの枠どころか影までの所になってワケワカメになるよ。
GtkWindow の decorated property を False で使うならいいけど。

Matrix 版と ClutterImage 版 comipoli との比較、
スクリーントーンの縮小も問題無し、心配不要だった。

結論、変わらねー。
せっかく勉強したんだから次の comipoli は Matrix でいこう。

PyGObject cairo

こんなページを見つけた。
Ubuntu忘備録: GdkPixbufを高速にリサイズ(cairo)

早くなるなら使ってみようかなと。
comipoli を PyGObject に戻すので Python3 で。

Gjs はリソースの cairo を使うけど PyGObject はモジュールを使う。
Pycairo は Fedora にはデフォルトで入っている。
Pycairo

手持ちの 7952x5304px な巨大画像を同じディレクトリに置いて以下を。

#!/usr/bin/env python3

import gi, sys, cairo, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file('7952x5304.jpg')
        self.resize(600, 300)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # method
        n = time.time()
        buf = self.pixbuf.scale_simple(aw//2, ah, GdkPixbuf.InterpType.HYPER)
        print('method  : {}'.format(time.time() - n))
        Gdk.cairo_set_source_pixbuf(cr, buf, 0, 0)
        cr.paint()
        # function
        i = time.time()
        buf2 = self.pixbuf_scale(self.pixbuf, aw//2, ah)
        print('function: {}'.format(time.time() - i))
        Gdk.cairo_set_source_pixbuf(cr, buf2, aw//2, 0)
        cr.paint()

    def pixbuf_scale(self, pixbuf, w, h):
        with cairo.ImageSurface(cairo.Format.ARGB32, w, h) as surface:
            cr = cairo.Context(surface)
            pw = pixbuf.get_width()
            ph = pixbuf.get_height()
            matrix = cairo.Matrix(w/pw, 0, 0, h/ph, 0, 0)
            cr.set_matrix(matrix)
            Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
            cr.paint()
            return Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

結果

三倍以上遅いんですけど、Python だからだろうか?
ちなみにこのマシンは i5-6500(skylake) 3.2GHz と内蔵グラフィックです。
このスペックでも 0.05 秒でリサイズできるのだから scale_simple で充分かと。

MessageBox.show @ Gjs and PyGObject

JavaScript の static メソッドはインスタンスからは呼び出しできない。

#!/usr/bin/gjs
 
class A {
    constructor(arg) {
        this.num = 'instance';
    }
    static static_method() {
        return 'staticmethod';
    }
}
A.num = 'static';
a = new A();
print(a.num); //=> instance
print(A.num); //=> static

print(A.static_method()); //=> staticmethod
print(a.static_method()); //=> ERROR!

当然である。
JavaScript の class は prototype の別表現でしかないから。

#!/usr/bin/gjs

function A() {}
A.func = function() {
    return 'staticmethod';
}
A.prototype.func = function() {
    return 'instancemethod';
}

print(A.func()); //=> staticmethod
let a = new A();
print(a.func()); //=> instancemethod

Python,C# 等の他言語ではインスタンスからでも呼び出しできる。

#!/usr/bin/env python3
 
class A:
    def __init__(self):
        self.num = 'instance'
    @staticmethod
    def static_method():
        return 'staticmethod'

A.num = 'static'
a = A()
print(a.num) #=> instance
print(A.num) #=> static

print(a.static_method()) #=> staticmethod
print(A.static_method()) #=> staticmethod

特に C# でよく見るけど、わざわざインスタンスを作ってからスタティックメソッドを使っている人の異様な多さを見ると JavaScript みたいなアクセスのほうがいいような。

そんなことより。
スタティックメソッドとインスタンスメソッドで同じ名前を指定してもいいんだ。
あまり自分のコードでスタティックを作らないから知らなかったよ。

ということで。
GtkMessageDialog で MessageBox.show を作ってみよう。
MessageBox.Show ではどちらの言語のコーディングスタイルにも合わないからね。
Gir の Gtk.Widget には show メソッドがあるけど無視できる。

#!/usr/bin/gjs

const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

var MessageBox = GObject.registerClass({
    GTypeName: "MessageBox"
}, class MessageBox extends Gtk.MessageDialog {
    _init(text) {
        super._init({
            buttons: Gtk.ButtonsType.OK,
            text: text,
            secondary_text: "test"
        });
    }
    static show(text) {
        //let dlg = new MessageBox(text); @ error!
        let dlg = new this(text);
        dlg.run();
        dlg.destroy();
    }
});

Gtk.init(null);
MessageBox.show("Message");

new this(text) って変な感じ。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class MessageBox (Gtk.MessageDialog):
    def __init__(self, text):
        Gtk.MessageDialog.__init__(
            self,
            buttons=Gtk.ButtonsType.OK,
            text=text,
            secondary_text="test" )
    @staticmethod
    def show(text):
        #dlg = self(text) @ error!
        dlg = MessageBox(text)
        dlg.run()
        dlg.destroy()

#Gtk.init(None) @ no need
MessageBox.show("Message")

PyGObject だとこう。

GUI を作っている時の print 代わりに。

GTK+ Overlay

Gedit を F11 でフルスクリーンにする。
その状態でマウスカーソルを上部に持って行くとフルスクリーンコントローラがスルスルと降りてくる。

これをどうやって実現しているのか今まで謎だった。
Clutter や非推奨の GtkFixed を使えば実は簡単なんだけど。
実際 0.1 や拙作 Clutter 動画プレイヤーのシークバーでやっているし。
Cocoa は GtkFixed みたいな古臭い API という事実は置いておいて。

GTK+ は基本的に Widget を配置すると引き伸ばしされる。
加えて GtkBin のサブクラスには Widget を一つしか配置できない。
つまり Widget 同士を重ねて配置することは不可能。
しかし Gedit 等は実現している、手段を調べてみよう。

実は Widget の引き伸ばしは単なるデフォルト動作だった。
知らなかったよマジで。
valign, halign プロパティのデフォルトが GTK_ALIGN_FILL なのだ。
つまり。

#!/usr/bin/gjs

const System = imports.system;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;

var FlWindow = GObject.registerClass({
    GTypeName: "FlWindow"
}, class FlWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        //
        let entry = new Gtk.Entry({
            text: "this is a Entry",
            valign: Gtk.Align.END
        });
        this.add(entry);
        //
        this.show_all();
    }
});

var FlApplication = GObject.registerClass({
    GTypeName: "FlApplication"
}, class FlApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("MyProgram");
        super._init({
            application_id: "palepoli.skr.jp",
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    }
    vfunc_startup() {
        super.vfunc_startup();
        new FlWindow(this);
    }
    vfunc_open(files, hint) {
        this.active_window.present();
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new FlApplication().run([System.programInvocationName].concat(ARGV));

で。

と valign, halign プロパティで Widget を上下左右に貼り付けることができる。
ウインドウをリサイズすると追従することが確認できる。

次は Widget 同士を重ねる方法。
なんてことはない、GtkOverlay というズバリな Widget があった。

#!/usr/bin/gjs

const System = imports.system;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;

var FlWindow = GObject.registerClass({
    GTypeName: "FlWindow"
}, class FlWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        // Widgets
        this.entry = new Gtk.Entry({
            no_show_all: true,
            text: "this is a Entry",
            valign: Gtk.Align.START
        });
        let view = new Gtk.TextView();
        view.buffer.set_text("1\n2\n3\n4\n5", -1);
        // Overlay
        let ol = new Gtk.Overlay();
        ol.add(view);
        ol.add_overlay(this.entry);
        this.add(ol);
        // Signal
        this.connect("motion-notify-event", (widget, event)=> {
            let [b, x, y] = event.get_coords();
            this.entry.visible = y < 50;
        });
        this.show_all();
    }
});

var FlApplication = GObject.registerClass({
    GTypeName: "FlApplication"
}, class FlApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("MyProgram");
        super._init({
            application_id: "palepoli.skr.jp",
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    }
    vfunc_startup() {
        super.vfunc_startup();
        new FlWindow(this);
    }
    vfunc_open(files, hint) {
        this.active_window.present();
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new FlApplication().run([System.programInvocationName].concat(ARGV));

結果。

クライアント領域上部にマウスカーソルを持って行くと Widget が重なって出て来る。
こんな感じでフルスクリーン時のみ Widget を重ねて上部に出してやればオケ。

後は Gedit のようにアニメーションで出す方法だけど。
ソースを見ると GtkRevealer を使っているので同じ配置をしてみたんだけど。
GTK_REVEALER_TRANSITION_TYPE_CROSSFADE 以外動かない、何故だ?
おまけに motion-notify-event で得る値がおかしくなる、ワカンネェ!
これについては気が向いたら後日。