Hatena::ブログ(Diary)

shouhの日記

2016-12-28

Python で呼び出し時のコマンドライン文字列を生で取得する

結論: Python の機能だけでは不可能なので OSAPI を使いましょう

Python の機能だけでは不可能?

Python の機能だけでコマンドライン文字列を取得するには sys モジュールの sys.argv を使う。

が、こやつには生のコマンドライン文字列を加工した結果が入っている。

その加工処理を Python インタプリタは問答無用で実行してしまう。だから生では取れない。

生で取得する for Windows

winapi の GetCommandLine() を使う。

pywin32 モジュールを使うなら win32api.GetCommandLine() で取れる。

生で取得する for Linux(調べてないです)

必要無かったので調べてないです。ごめんなさい。

おまけ: GetCommandLine() の仕様

GetCommandLine() の実行結果はよく調べておかないとハマると思う。つーかハマった。

具体的に言うと、PATH や PATHEXT が補われる場合に、ちょっと中身が変わる。以下参照。

$ python print_getcommandline.py hoge fuga 1
[python  print_getcommandline.py hoge fuga 1]

$ print_getcommandline.py hoge fuga 1
["c:\bin1\Python27\python.exe" "c:\work\slot\python\print_getcommandline.py"  hoge fuga 1]

$ print_getcommandline hoge fuga 1
["c:\bin1\Python27\python.exe" "c:\work\slot\python\print_getcommandline.py"  hoge fuga 1]

2016-11-13

Python の GUI フレームワーク Tkinter, wxPython, Kivy の比較

Tkinter, wxPython, Kivy と GUI フレームワークを3つほど使ってみたので、整理も兼ねてちょいと比較という観点でまとめてみた。

比較


ライブラリ新しさ デザイン 更新 学習の難易度 複雑なGUIを組めるか
kivy 新しい かっこいい 活発 難しい 可能?
wxPython 古い ややださい 停止 普通 ある程度は可能
Tkinter 超古い 超ださい 活発? 簡単 きついかも

kivy は後発のモダンGUI フレームワーク。デザインがカッコイイのと、スマホ向けにも対応しているのが特徴だけど、まだ枯れてないこともあり、日本語情報もなかったりして学習難度が高い。

Tkinter は Python 標準付属のブツ。標準で使えるのは有り難いし、標準なので 3.x でもサポートされているので安心だし、シンプルな機能性と豊富な情報源により学習難度も低い。しかしデザインのダサさに不満が大きいのと(だからこそ他の GUI フレームワークが活発化した)、シンプルゆえにあまり凝った GUI を書くのが難しい。

wxPython は kivy と Tkinter の中間みたいなブツ。Tkinter よりも学習難度は高いが、多少は凝ったことができる(有名な wxPython 製として BitTorrentGoogle Drive など)。ただデザインは依然としてダサイみたい。

所感

WindowsPython で簡単な GUI 使いたいなーという私の所感。

kivy は日本語情報が乏しくて辛すぎる。挫折した。

wxPython は普通に使えそう。だけどちょっと機能が複雑で学習が辛いのと、ライブラリインストールが必要なのがマイナス。もうちょっと楽したいかなあ。デザインは別に気にならない。Windowsクラシックみたいな感じ。

Tkinter は三者の中では一番楽。標準で入ってるし、機能シンプルで学習コストも割と低い。デザインは気にならない。Windowsクラシックみたいな感じ。ただスクロール周りなどが若干不便なのは何とかしたいところ。

参考

インクリメンタルサーチでリストボックスの表示内容を変える、みたいな GUI を作ってみることで試用した記事があります。

Python の GUI フレームワーク Tkinter を使ってみたけど良い感じ in Windows7 and Python2.7

KivywxPython に続き、Tkinter も使ってみた。

f:id:shouh:20161113191954j:image

前回と同じく、上画像のような(インクリメンタルサーチ結果をリストボックスに反映する)GUI を作ってみた。なお、コードは最後。


Good

Bad

GUI の挙動が微妙に不便。これに尽きる。

詳細は下記するが、普段使ってる GUI と同じ感覚で使ってると「あれ、これはできないんだ……」ってのが割とある。それをできるようにするためには小細工やらオプション指定やらが必要で、英語サイトを読むことになる(日本語サイトではこの細かいレベルの情報が見つからない)。まあ一度解決してしまえば、あとはおまじないなり関数化するなりして使い回せばいいのだが。

Bad例: スクロールバーの取り扱いが面倒

たとえば ListBox にスクロールバーを付けたい場合、Scrollbar なるクラスのインスタンスをつくって、あれこれ設定して、ListBox のインスタンスと関連付ける……みたいな面倒くさい手順が必要。

ここは ListBox をつくるだけで自動的にスクロールバーもついて欲しいところ。

Bad例: Entry(一行入力ボックス)が Ctrl+A(全選択) や Ctrl+Z(UNDO) を実装していない

デフォルトのキー動作が色々とおかしい。

Ctrl + A は全選択が普通なのになぜかカーソル先頭移動だし、Ctrl + D でなぜか一文字消去(BackSpace) だし、Ctrl + Z は元に戻すのが普通なのに効かないし……と、よくわからない動作となっていて不便。

これに対処するには、キーバインド(このキーを押したらこの関数を呼べ)を自分で定義する必要がある。たとえば Ctrl + A のキーを押したら全選択を行う関数を呼べ、という感じで。ちなみに、この前選択を行う関数とやらも自分で作り込む必要がある(といって Entry のメソッドが色々あるのでそれを上手く組み合わせればいい)。

Bad例: スクロールに Wheel Redirect が効かない

Wheel Redirect とは「アクティブでないウィンドウに対してホイールスクロールする」機能であり、かざぐるマウス等のフリーソフトで実現できる。

これが Tkinter の GUI には効かない。スクロールしたいなら、いちいちアクティブにしてからスクロールしなきゃいけない。地味に不便。

Bad例: Tab キー移動を併用すると ListBox から選択した値を取得できない

ListBox から選択した値を取るには curselections() メソッドを使えばいい。(idx, value) のタプルが取れる。しかし、以下のような変な挙動がある。

  • Tab キーで ListBox からフォーカスを移すと、選択状態も消えるらしく空タプルが返る
  • マウスでフォーカスを移すと、選択状態が消えたり消えなかったりする
    • 選択状態が残ってる時でも (3, ) のように value が取れない

これを解決するには、ListBox 生成時に exportselection=0 というオプション指定をすればいい(参考: https://bytes.com/topic/python/answers/20254-tkinter-listbox-looses-selection-tab )

Bad例: ×ボタンで終了できない

解決にちょっと苦労した。

答えは root.protocol('WM_DELETE_WINDOW', self.quit) みたいな感じで、WM_DELETE_WINDOW というウィンドウメッセージ(?)に終了用関数(ここでは quit )をバインドしてやればいい、とのこと。

レイアウトについて(位置編)

そういえばレイアウトについて言及してなかった。ちょっと話変えて言及します。

GUI 部品のレイアウトは、

  1. Frame という土台みたいなのを作る
  2. GUI 部品の pack() メソッドを呼び出すことで設定する
    • もっというと side というパラメータに top, bottom, right,left のいずれかを指定する

という感じで配置していく。以下、例を示してみる(Frameは既に配置してるとします)。

例1: 下方向にずらり

self.button1.pack({'side':'top'})
self.button2.pack({'side':'top'})
self.button3.pack({'side':'top'})

↓どう配置される?

[button1]
[button2]
[button3]

例2: 横方向にずらり

self.button1.pack({'side':'left'})
self.button2.pack({'side':'left'})
self.button3.pack({'side':'left'})

↓どう配置される?

[button1][button2][button3]

例3: クイズその1

self.button1.pack({'side':'top'})
self.button2.pack({'side':'bottom'})
self.button3.pack({'side':'top'})
self.button4.pack({'side':'bottom'})

↓どう配置される?

[button1]
[button3]
[button4]
[button2]

例3はちょっとわかりにくいが、先に配置した部品をまたがない範囲で配置ってことになるのだと思う。例3でいえば、button3 は top に配置しようとしているが、既に button1 と button2 が配置されているので、その間の top(つまりは上方)に配置されようとする。button4 も同様で、button1, 2, 3 の間の bottom、もっというと button3 と button2 の間に配置されようとする。

例4: クイズその2

self.button1.pack({'side':'top'})
self.button2.pack({'side':'bottom'})
self.button3.pack({'side':'right'})
self.button4.pack({'side':'left'})

↓どう配置される?

    [button1]
[button4][button3]
    [button2]

……とまあ、こんな感じ。この挙動を見れば感づくかもしれないが、Grid 状に並べることはできない。並べたいなら、Frame を複数作って対応する。


[frame1]
[frame2]

↓ frame1 に button1, 2 を、
↓ frame2 に button3, 4 を配置する

[button1][button2]
[button3][button3]

レイアウトについて(サイズ編) ※ぶっちゃけまだ理解してません

部品の位置は pick メソッドの side パラメータに top/bottom/left/right で配置するってのはわかった。じゃあ各部品のサイズはどうなる?

これも pick メソッドを使う。今度は side パラメータではなく fill パラメータ

self.button1.pack({'side':'top', 'fill':'x'})
self.button2.pack({'side':'top', 'fill':'y'})
self.button2.pack({'side':'top', 'fill':'both'})

みたいな感じで、x方向にfill(めいっぱい伸ばす)するのか、y方向にするのか、はたまた both(両方向)なのか……を指定する、みたいなのだが。

色々試してみてもいまいちよーわからんかった。本記事のコードもぶっちゃけテキトーというか、「これ指定したら上手くいったわ」って感じでやってます(苦笑)。もっと勉強しないとね。。。

レイアウトについて: 絶対座標指定はできないの?

できないみたいです。調べてみても見当たらなかった(あるいは読解力が無いだけかもしれませんが)。

コード

レイアウト言及おしまい。最後にコード。

# -*- coding: utf-8 -*-

from Tkinter import *

class ListBoxSelector:
    def __init__(self, tkinter_listbox):
        self._lb = tkinter_listbox
        self._update()

    def _update(self):
        selection = self._lb.curselection()
        self._idx = -1
        self._value = ''

        if len(selection)!=0:
            self._idx = int(selection[0])
            self._value = self._lb.get(self._idx)

    def _get_len(self):
        return self._lb.size()

    def _set_idx(self, idx):
        self._lb.selection_clear(0, self._get_len())
        self._lb.selection_set(idx)
        self._lb.see(idx)
        self._update()

    def set_initial_pos(self):
        self._set_idx(-1)

    def down(self):
        new_idx = self._idx + 1
        if new_idx >= self._get_len():
            return
        self._set_idx(new_idx)

    def up(self):
        new_idx = self._idx - 1
        if new_idx < 0:
            return
        self._set_idx(new_idx)

    def get(self):
        self._update()
        return self._idx, self._value

class Application(Frame):
    def __init__(self, root=None):
        self.root=root

        self._on_enter = None
        self._on_text = None

        self._result_selector = None
        self._search_func = self._default_search_func
        self._contents_org = [] # incremental search 用に元リストを保持.

        # 'X' button quitting.
        root.protocol('WM_DELETE_WINDOW', self.quit)

    def _default_focus(self):
        self.querybox.focus()

    def _default_search_func(self, strlist, query):
        if len(query)==0:
            return strlist
        if query[0]==' ':
            return strlist

        ret = []
        for line in strlist:
            if line.find(query)!=-1:
                ret.append(line)
        return ret

    def _default_reflect_func(self):
        items = self._contents_org
        query = self.querybox.get()
        print 'len items:%d' % len(items)

        new_items = self._search_func(items, query)
        self.set_contents(new_items, update_org=False)

        # 検索結果が変わるはずなんでいったんクリア.
        self._result_selector.set_initial_pos()

    # public
    # ------

    def create(self):
        Frame.__init__(self, root)
        self.pack()

        # widget support
        # --------------
        self.querybox_sv = StringVar()
        def on_text(name, index, mode, sv=self.querybox_sv):
            text = sv.get()
            self._on_text(text)
            self._default_reflect_func()
        self.querybox_sv.trace('w', on_text)

        # widget def
        # ----------
        self.querybox = Entry(master=self.root, textvariable=self.querybox_sv)
        self.searchresult = Listbox(master=self.root,
                                    exportselection=0)
        self.yscroll = Scrollbar(master=self.root, orient=VERTICAL)

        # packing
        # -------
        self.querybox.pack({'side':'top', 'fill':'x'})
        self.yscroll.pack({'side':'right', 'fill':'y'})
        self.searchresult.pack({'side':'top', 'fill':'x'})

        # listbox
        # -------
        # scrollbar relationship
        self.yscroll.config(command=self.searchresult.yview)
        self.searchresult.config(yscrollcommand=self.yscroll.set)
        # status controller relationship
        self._result_selector = ListBoxSelector(self.searchresult)

        # binds
        # -----
        def on_enter(ev):
            idx, value = self._result_selector.get()
            self._on_enter(idx, value)

        def on_key(ev):
            k = ev.keycode
            if k==38: # up=38
                self._result_selector.up()
                return 'break'
            if k==40: # down=40
                self._result_selector.down()
                return 'break'

        def selectall(ev):
            self.querybox.select_range(0, END)
            return 'break'

        def do_nothing(*args, **kwargs):
            return 'break'

        self.querybox.bind('<Return>', on_enter)
        self.querybox.bind('<Control-a>', selectall)
        self.querybox.bind('<Control-d>', do_nothing)
        self.querybox.bind('<Control-/>', do_nothing)
        self.querybox.bind('<KeyPress>', on_key)

        # rest preperation
        # ----------------
        self._default_focus()

    def set_on_enter(self, on_enter):
        """ on_enter(idx, value) """
        self._on_enter = on_enter

    def set_on_text(self, on_text):
        """ on_text(new_string) """
        self._on_text = on_text

    def set_contents(self, contents, update_org=True):
        """ @param contens A string list.
        Must be called after create(). """
        if update_org:
            import copy
            self._contents_org = copy.deepcopy(contents)

        sr = self.searchresult
        size = sr.size()
        sr.delete(0, size)

        for elm in contents:
            sr.insert(END, elm)

root = Tk()
root.title('いんくりめんたるさーち')
root.geometry('320x100+0+0')

app = Application(root=root)
def on_text(new_stirng):
    print 'changed!: new=[%s]' % new_stirng
app.set_on_text(on_text)
def on_enter(idx, value):
    print '%d, %s' % (idx, value)
app.set_on_enter(on_enter)
app.create()
app.set_contents(['data%d'%elm for elm in range(32)])

app.mainloop()
root.destroy()

2016-10-19

Python の GUI フレームワーク wxPython を使ってみたけど良い感じ in Windows7 and Python2.7

仕事で Windows + PythonGUI を組む必要が出た。以前、この点のフレームワークとして新しめの kivy を試した のだが、ムズくて断念した。今回は昔から定評があるらしい wxPython を使って、

f:id:shouh:20161019074454j:image

こんなのを作ってみた。「入力内容に部分一致した文字列のみをインクリメンタルサーチで表示する」 GUI。fenrir や CraftLaunch 等を使ってる人には馴染みがあると思う。

これを作ってみた感じたことや思ったことをGood/Bad付きで雑多に書きたい。wxPython の紹介よりも、もう一歩実用面に踏み込んだ話になると思う。一部 wxPython 関係無く GUI プログラミング一般論になっているかもだがご容赦を。

なおソースは一番最後に記載。


Bad 古い

公式サイト を見る限り、最近は全然更新されてない。一番新しいのでも「(28-Nov-2014) wxPython (classic) 3.0.2.0 has been released. 」と2014年だし。

また wxPython は Python3 で使えない。いや、使えないことはないみたいだけど色々設定が面倒くさそう(詳しく調べてないですすみません)。

どんどんレガシー化していくことだろう。唯一の希望は 次世代wxPython「Phoenix」 だけど、いつ実用化されるのやら。

Good レイアウト設定は割と簡単

Frame、Panel、BoxSizer など覚えることは結構あるけど レイアウトの組み合わせ(wxPython) - Python入門から応用までの学習サイト のような日本語サイトも多数あるので、英語サイトしかない kivy みたいに苦戦しない。一からちゃんと積んでいけば英語できなくても理解できる。

Bad とりあえずサンプル動かすのは簡単、だけど……

日本語情報が豊富なので、とりあえずサンプルを動かすまでは簡単。

でも、もっと実践的なノウハウだったり、「〜〜を実現したいけどどうすればいいの?」など痒いところに手を届かせるとなるとムズイ。問題解決するために 英語のリファレンス を見てみたり、そこにも書いてないものはググって出てきた海外サイトの記事を読んだり、定数定義などは直接ソース見たり(私は (Pythonディレクトリ)\Lib\site-packages\wx-3.0-msw\wx 配下を GREP したりした)、実行して動かしているオブジェクトの中身を dir 関数で見てみたりして探したり、など結構行ったり来たりする。

Bad コマンドライン実行(外部プログラム呼び出し)に弱い

Pythonコマンドライン実行を行う場合は os.system やら subprocess.call やら subprocess.Popen を使うと思う。けど、wxPython アプリ実行中にこれらを使うとwxPython アプリ側がフリーズしてしまう。ちなみに呼び出したアプリを終了するとフリーズは直る。

「非同期実行してないだけじゃ?」と思われるかもしれないが、そうでもない。非同期実行してもフリーズする。単なる待ち状態じゃなくてフリーズ。Windows も「応答無し」と判断しちゃう。せめてもの救いは CPU 使用率をフル消費しないことだろうか。

フリーズを防ぎたいなら wx.Execute(commandline) を使えばいい。が、こいつはコマンドライン実行としてはかなり貧弱である。たとえば c:\windows とフォルダパスを指定しても開けないし、 d:\data\text\memo.txt とデータファイルを指定しても関連付けられたエディタで開いてくれない。プログラムランチャー等を作りたい場合はかなり苦しいと思う。

解決策は不明。たぶん、フリーズを食い止める方法があるとは思うんだけどなー(スレッドを使う方法は試したけどダメだった)。

Bad フォント設定が曲者

フォント設定について制約がきつい。

  • 一度だけ設定すればウィンドウ内全体に適用される、ではなく作った部品それぞれに設定しなきゃいけない
    • 例: テキストボックスやらリストボックスが10個あれば10個全部に設定が必要
  • フォント名の指定ができない
    • wx.Font を見ればわかるとおり、wx.FONTFAMILY_DEFAULT だの wx.FONTFAMILY_SCRIPT だのフォントファミリー名(?)を指定するようになっていて、具体的なフォント名が使えない

前者はともかくフォント名指定は仕方ない気がする。wxPython はクロスプラットフォームを目指して作られているので、「MS 明朝を指定したい!」みたいな OS 固有(Windows にしか存在しないフォント)操作はできないのだろう。個人的にはそういう口があっても良い気がするけど。

Good 性能面もまあ悪くない

今回作ったソースも起動に1秒くらいしかかからないし、ListBox の要素も1000要素くらいならサクサク表示・操作できた(10000は数秒以上のタイムラグがあった)。

……まあこれだけしか試してないけど、短気な私が「重くてだめじゃん」と感じなかったので悪くないと評価しておく。

Good カスタマイズも割と柔軟

さすが何年も前から使い続けられてるだけあって、痒いところにもやや手が届く。「この部品のここをこうすることはできないか」的な要望は、割と準備されていると思う。ただ、その準備されているかどうかを知るのがちょっとムズイけど。リファレンス に書いてあればまだマシで、書いてないこともある。

なお、「やや手が届く」と書いたのは、完全には届かないから。wxPython はクロスプラットフォームを目指して作ってあり、OS 固有の事情を隠す実装となっているので、OS 固有の機能を使おうとなった途端にハードルが上がる(というか無理?)Windows で言うなら、Windows API を自由に呼び出して挙動を制御するレベルのことは当然できない。たとえばウィンドウを作る際に CreateWindow 関数に指定できるパラメータのバリエーションと同じレベルのバリエーションで設定したい!というのは無理である。

Good? Bad? デザインは良くも悪くもシンプル

Windowsクラシックスタイルを彷彿とさせる。私は Web やスマホ界隈のポップなデザインが嫌いなので、wxPython のデザインは好き。まあ好みでしょう。

Bad コールバック関数にコンテキストを渡すのがちょっとムズイ

wxPython に限らずイベントドリブンな GUI プログラミングは「ボタンを押した時に実行する関数を指定しておく」ことで、ボタンを押した時の処理を登録する。この際、ボタンを押して実行される関数には、今動いてるオブジェクトなど必要な情報を渡してやる必要があるのだが……この情報渡しがちょっとムズイ。

プログラミング言語のムズイ概念(レキシカルスコープやらダックタイピングやら)を使わないといけない。この辺の知識をなあなあでやってると詰むかハマります。

もっともこの辺はちゃんとしたイディオム(こう書けばいいよという定石)があるのかもしれないけど、私は見つけられなかった。

Good? Bad? 文字列は unicode string を使う

Python2.x の文字列には byte string("string")と unicode string(u"string") があるが、wxPython では全面的に unicode string を使う。

私は byte string ばっか使ってるのでちょっと使いづらかった。が、Python一般論では unicode string を使う(byte string は入出力する時に変換するだけ)のが常識なので、理には適っていると思う。

おわりに

見た目はちょっと味気ないけど、GUI フレームワークとして枯れていて、使いやすさも品質も優れていると思う。ただし古いのでこの先ずっと使えるとは思わない方がよさそう。

しかし元々クラスプラットフォーム向けということもあり、OS 固有機能のレベルで痒いところに手を届かせることができないことには注意が必要。

ソース

今回つくった GUI のソース。一応汎用的に使えるように部品化したつもり。下手なコードと英語は相変わらずなのでご容赦を。

# -*- coding: utf-8 -*-

import wx

def execute(*args, **kwargs):
    """ If you can use a command-line execution,
    you must be call it via this function.
    Otherwise, the wx application hangs-up... """
    return wx.Execute(*args, **kwargs)

class WindowConfig:
    def __init__(self, title, size, pos, wx_font_prms=None):
        """ @param title A unicode string.
        @param size A tuple.
        @param pos A tuple.
        @param wx_font_prms A tuple of wx.Font.__init__() """
        self.title = title
        self.size = size
        self.pos = pos
        self.wx_font_prms = wx_font_prms

    def __str__(self):
        return """title={0}
pos={1}, size={2}
font={3} """.format(self.title, self.pos, self.size, self.wx_font)

class ListBoxSelector:
    def __init__(self, wx_listbox):
        self._lb = wx_listbox
        self._idx = self._get_curidx()

    def _get_curidx(self):
        return self._lb.GetSelection()

    def _get_len(self):
        return len(self._lb.GetItems())

    def _set_idx(self, idx):
        self._idx = idx
        self._lb.SetSelection(idx)

    def set_initial_pos(self):
        self._set_idx(-1)

    def down(self):
        new_idx = self._idx + 1
        if new_idx >= self._get_len():
            return
        self._set_idx(new_idx)

    def up(self):
        new_idx = self._idx - 1
        if new_idx < 0:
            return
        self._set_idx(new_idx)

class IncrementalSearcher:
    def __init__(self, wc):
        self._on_enter = None
        self._on_escape = self.exit
        self._selections = []
        self._search_func = self._default_search_func

        self._set_windowconfig(wc)

        self._wx_app = wx.App()

        self._use_enter_exit = False

        # hidden params
        # -------------
        # ListBox のYサイズはウィンドウYサイズ以上の wx.GROW が効かないので
        # ここで定めた最大値をあらかじめ設定する方式を採用.
        self._wx_listbox_maxysize = 1000

    # private
    # -------

    def _default_search_func(self, ustrlist, query):
        if len(query)==0:
            return ustrlist
        if query[0]==' ':
            return ustrlist

        ret = []
        for line in ustrlist:
            if line.find(query)!=-1:
                ret.append(line)
        return ret

    def _set_windowconfig(self, wc):
        if wc.title:
            self._title = wc.title
        if wc.size:
            self._xsize = wc.size[0]
            self._ysize = wc.size[1]
        if wc.pos:
            self._xpos = wc.pos[0]
            self._ypos = wc.pos[1]

        if wc.wx_font_prms:
            self._wx_font_prms= wc.wx_font_prms
        else:
            self._wx_font_prms = None # use system default

    def _assert_parameters(self):
        def error(msg):
            raise RuntimeError('Invalid parameter {0}'.format(msg))

        if not callable(self._on_enter):
            error('on_enter')
        if not callable(self._search_func):
            error('search_func')

    def _on_keydown(self, ev):
        code = ev.GetKeyCode()
        if code==wx.WXK_UP:
            self._selector.up()
            return
        if code==wx.WXK_DOWN:
            self._selector.down()
            return
        if code==wx.WXK_ESCAPE:
            self._on_escape()
            return
        ev.Skip()

    def _on_change_querybox(self):
        items = self._selections
        query = self._wx_querybox.GetLineText(lineNo=0)

        new_items = self._search_func(items, query)
        # print 'query="{0}", {1} matched.'.format(query, len(new_items))
        self._wx_searchresult.Set(new_items)

        # 結果表示が変わるはずなんでいったんクリア.
        self._selector.set_initial_pos()

    # public
    # ------

    def set_on_enter(self, on_enter_func):
        """ @param on_enter A callback function on pressing enter key.
        * Params must be `on_enter_func(selected_item)`
          * `selected_item` is a unicode string.
          * if not selected, `selected_item` becomes None.
        * No returns required. """
        self._on_enter = on_enter_func

    def set_on_escape(self, on_escape_func):
        """ @param on_enter A callback function on pressing escape key.
        * Params must be `on_enter_func()`
        * No returns required. """
        self._on_escape = on_escape_func

    def set_selections(self, selections):
        """ @param selections A unicode string list. """
        import copy
        self._selections = copy.deepcopy(selections)

    def set_search_func(self, search_func):
        """ @param search_func A function with search implementation.
        * Params must be `search_func(unicode_string_list, query)`
          * `query` must be a unicode string.
        * Returns must be a string list. """
        self._search_func = search_func

    def use_enter_exit(self):
        self._use_enter_exit = True

    def start(self):
        # Unhandled exception in thread started by
        # <bound method IncrementalSearcher._start of <incremental_search.IncrementalSearcher instance at 0x02CC2A58>>
        #import thread
        #thread.start_new_thread(self._start, ())
        self._start()

    def _start(self):
        self._assert_parameters()

        # window
        # ------
        self._wx_frame = wx.Frame(
            None,
            wx.ID_ANY,
            title=self._title,
            size=(self._xsize, self._ysize),
            pos=(self._xpos, self._ypos),
        )

        # panel
        # ------
        root_panel = wx.Panel(self._wx_frame, wx.ID_ANY)
        input_panel = wx.Panel(root_panel, wx.ID_ANY)
        display_panel = wx.Panel(root_panel, wx.ID_ANY)

        # components
        # ----------
        self._wx_querybox = wx.TextCtrl(parent=input_panel, id=wx.ID_ANY,
                                        style=wx.TE_PROCESS_ENTER)
        self._wx_querybox.SetValue('')

        self._wx_searchresult = wx.ListBox(
            display_panel, wx.ID_ANY,
            choices=self._selections,
            style=wx.LB_SINGLE | wx.LB_NEEDED_SB,
            size=(self._xsize, self._wx_listbox_maxysize)
        )

        self._selector = ListBoxSelector(self._wx_searchresult)

        # set font settings
        if self._wx_font_prms:
            # * wx.Font をつくるには wx.App が必要だが
            #   利用者側に wx.App をつくらせるのは易しくないので回避.
            #   (wx.Font はここで作るようにして, 利用者はパラメータを渡す.)
            wx_font = wx.Font(*self._wx_font_prms)
            self._wx_querybox.SetFont(wx_font)
            self._wx_searchresult.SetFont(wx_font)

        # layout
        # ------
        root_layout = wx.BoxSizer(wx.VERTICAL)
        root_layout.Add(input_panel, flag=wx.GROW)
        root_layout.Add(display_panel, flag=wx.GROW)
        root_panel.SetSizer(root_layout)

        input_layout = wx.BoxSizer(wx.HORIZONTAL)
        input_layout.Add(self._wx_querybox, proportion=1)
        input_panel.SetSizer(input_layout)

        display_layout = wx.BoxSizer(wx.VERTICAL)
        display_layout.Add(self._wx_searchresult, flag=wx.GROW)
        display_panel.SetSizer(display_layout)

        # binds
        # -----

        def on_change_querybox(e):
            self._on_change_querybox()
        def on_keydown(e):
            self._on_keydown(e)
        def on_enter_query(e):
            """ @return None if not selected. """
            wx_listbox = self._wx_searchresult
            idx = wx_listbox.GetSelection()
            item = None
            if idx!=-1:
                item = wx_listbox.GetItems()[idx]
            self._on_enter(item)
            if self._use_enter_exit:
                self.exit()

        cls = IncrementalSearcher
        self._wx_querybox.Bind(wx.EVT_TEXT_ENTER, on_enter_query)
        self._wx_querybox.Bind(wx.EVT_TEXT, on_change_querybox)
        self._wx_querybox.Bind(wx.EVT_KEY_DOWN, on_keydown)

        # start
        # -----
        self._wx_frame.Show()
        self._wx_app.MainLoop()

    def exit(self):
        self._wx_app.Exit()

    def clear_query(self):
        self._wx_querybox.SetValue('')
        self._on_change_querybox()

    def select_query(self):
        self._wx_querybox.SelectAll()

    def debug(self):
        sample = 'what is this text size?'
        print sample
        w, h = self._wx_querybox.GetTextExtent(sample)
        print 'querybox : width:%d, height:%d' % (w, h)
        w, h = self._wx_searchresult.GetTextExtent(sample)
        print 'searchres: width:%d, height:%d' % (w, h)

if __name__=='__main__':
    # Use sample.

    def rnd(n):
        import random
        return random.randint(0, n-1)

    def rndchar():
        # 【注意】本当はハートマークとチェックマークも含めていましたが、
        # ブログ記事の文字コードの関係で表示できないため
        # 「?」として記述しています。
        chars= u'abcdefghijklmnopqrstuvwxyzあいうえお†??'
        return chars[rnd(len(chars))]

    def rndword(length):
        ret = u''
        for i in range(length):
            ret += rndchar()
        return ret

    def custom_search_func(ustrlist, query):
        if len(query)==0:
            return ustrlist
        if query[0]==' ':
            return ustrlist

        ret = []
        for line in ustrlist:
            if line.startswith(query):
                ret.append(line)
        return ret

    def on_enter(item):
        print 'on_enter:%s' % item

    list_contents = [rndword(8) for i in range(100)]
    # wx_font_prms = (20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
    wx_font_prms = None

    wc = WindowConfig(
        title=u'いんくりめんたるさーち',
        size=(320, 100),
        pos=(0, 0),
        wx_font_prms=wx_font_prms,
    )
    gui = IncrementalSearcher(wc)
    gui.set_on_enter(on_enter)
    gui.set_selections(list_contents)
    #gui.set_search_func(custom_search_func)
    #gui.set_on_escape(gui.clear_query)
    #gui.set_on_escape(gui.select_query)
    gui.set_on_escape(gui.debug)
    gui.use_enter_exit()
    gui.start()

2016-10-15

Python の GUI フレームワーク kivy を使ったけどムズすぎて断念した in Windows7 and Python2.7

PythonGUI を組む必要があったので、せっかくだし新しめのフレームワークを使ってみようということで kivy に目をつけたのですが……使うのがムズすぎて断念しましたって話。

最初に結論

思い通りにレイアウトをつくるのがムズすぎる。

kivy には BoxLayout(縦or横に並べる)、GridLayout(NxMマス上に並べる)、AnchorLayout(四隅と上下左右の端に並べる) など「どう並べるか」を簡単に設定する仕組み(詳しくは英語ですが この辺を )があるのですが、それだけです。

レイアウト設計には「並べる部品自体のサイズ」と「どこから並べ始めるか」も必要なわけですが、kivy はこの二点が弱い。というより、どうやって設定すればいいかわからん。「どう並べるか」が簡単にできるなら、これら二点についても簡単に行えるようにしてほしかったですね。

「どこから並べ始めるか」について

レイアウトの常識は

  • 画面左上が(0,0)で、常にここから並べ始める
  • X方向は右方向が正(左方向が負)
  • Y方向は下方向が正(上方向が負)

だと思いますが、kivy はこうなっています。

  • 画面左下が(0,0)
  • X方向は右方向が正(左方向が負)
  • Y方向は下方向が負(上方向が上)

なので、考えなしにレイアウト作ってると、配置したパーツ達は下側に集まります。これはちょっといかがなものかなと。

普通は上記常識に沿って上側に集まるべきです。その方がタイトルバーと近くて直感的に操作しやすいですし、下側の余白はウィンドウのYサイズを小さくすればなくせます。kivy のように下側に集まってると、タイトルバーと部品群の間に隙間があって違和感ありまくりです。ウィンドウサイズを小さくしようにもできません。

ここで「じゃあ内部変数とか使って画面左上に来るよう調整すればいいでしょ」とツッコミが入るかもしれませんが、GUI フレームワークなのにそんな微調整をプログラマにやらせるのでしょうか。微調整がつらいからフレームワークにラップさせるんじゃないでしょうか。

「並べる部品自体のサイズ」について

レイアウト設計において重要なのがもう一つ。「並べる部品自体のサイズ」です。

部品のサイズは最悪一つ一つ手打ちで「部品Aのサイズは(200,300)、部品Bは(300,30)……」と決めることもできますが、これだと画面がちょっと変わる度に微修正を加える必要があり大変です。

そこで継承(親要素の値を参照する)と相対指定を使います。「部品AのXサイズはウィンドウXサイズの1/2、YサイズはウィンドウYサイズの1/5……」というふうに、親要素(ここではウィンドウ)の値を参照した上で、そこからの相対値で決めるわけです。そうすれば親要素が変更されても自動で追従されるため変更の手間がありません。

この継承と相対指定、kivy でも当然使えはするのですが……継承がムズすぎる!

どこを見れば親要素の値を取れるのかがよくわかりません。API Reference を読んでいくと、size_hint だの size だの width だと height だ色んなパラメータが出てきます。しかも英語だし、説明も抽象的で具体的サンプルもあまり無い。全然ピンと来ません。

手を動かして試そうにも、kivy さんはおかしいパラメータを指定した時にエラーを吐いてくれない(デバッグログにも出ない)ので、何がおかしいかもわからないのです。

部品サイズの初期値がテキトーすぎる

もう一つダメ出しすると、部品サイズの初期値も意味不明です。個人的には親要素をちょうど埋めるサイズにしてほしいのですが、そうではなくテキトーな値になってます。

そうですね、たとえば Grid レイアウトで 2x2 を設定して、ボタンを四つ配置したとすると、画面を四等分してボタン四つを配置してほしいじゃないですか。でも kivy ではそうはならない。テキトーなサイズが設定されてますので、

f:id:shouh:20161015090356j:image:medium

↑ こんなことになるんです(コードは末尾に載せてます)。

これではちょっとした GUI を作るのにも、いちいちサイズを手打ちするなり親要素からの継承と相対指定するなりしなきゃいけない。面倒くさい。しかも後者は、上記にも書いたとおりハードルが高い。

まとめ

kivy で思い通りのレイアウトをつくるのは大変です。少なくとも私レベルでは6時間かけても歯が立ちませんでした。プログラミング経験の浅い人や(他言語の造形が深くない)Python 初心者が挑むのは正直ムリだと思います。

日頃から GUI をガリガリ書いてるような玄人さんなら使えると思います。そうでなければ頭の体操や謎解きゲームとして挑んでみると面白いのではないでしょうかあはは。……まあ、GitHub で 1000 以上のスターがついてるんで、ちゃんと使えるようになっていると思います。たぶん私が未熟なせい。

とりあえず他に為す術が思い浮かばないので、kivy の採用は見送ります。もっと流行って、日本語ドキュメントも増えて、上記の不便を解消するライブラリやらスニペットやらイディオムからも出てきたら、私でも使えるようになるのかな。

それはそうと、他の候補は何があるだろう?wxPython かしらん。

参考: コード

buttonlauncher.py

# -*- coding: utf-8 -*-

import kivy
kivy.require('1.9.0')

from kivy.app import App
from kivy.config import Config
from kivy.uix.widget import Widget
from kivy.uix.label import Label

class MyWidget(Widget):
    def __init__(self):
        super(MyWidget, self).__init__()

class ButtonLauncherApp(App):

    def build(self):
        self.root = MyWidget()
        root = self.root
        return root

if __name__ == '__main__':
    ButtonLauncherApp().run()

buttonlauncher.kv

#:kivy 1.9.0

<MyWidget>:
    GridLayout:
        rows: 2
        cols: 2

        Button:
            text: 'button1'
        Button:
            text: 'button2'
        Button:
            text: 'button3'
        Button:
            text: 'button4'

2016-09-30

Windows の 32bit 64bit と Python の 32bit 64bit はどう組み合わせればいい?

WindowsPython を使ってて遭遇する疑問の一つが「Python にも 32ビット(x86) と 64 ビット(x86_64)がある」ことだろう。一方、Windows 自体にも 32 ビットと 64 ビットがあるわけで、以下の選択肢がある。

  • win32 と py32
  • win32 と py64
  • win64 と py32
  • win64 と py64

どの組み合わせがいいんだろうか?……この辺、いまいち理解してなかったので、重たい腰を上げて調べてみた。

※以降では Windows 32 ビットを win32、Python 32 ビットを py32 などと省略して書きます

まずは結論

可能な組合せは以下のとおり。

  • win32 と py32 → OK
  • win32 と py64 → NG
  • win64 と py32 → OK
  • win64 と py64 → OK

まず win32 の場合は py32 一択。py64 は使用不可能。

ややこしいのは win64。win64 では py32 も py64 も両方使える。各々のメリット、デメリットは以下。

win64 と py32

  • メリット
  • デメリット
    • WOW64というエミュレータ経由で動くため動作が遅い

win64 と py64

  • メリット
    • 巨大なデータを扱えるため数値計算、科学技術計算、画像処理などに重宝
      • (参考)py32 だと sys.maxint は 2147483647、py64 だと 9223372036854775807
    • WOW64 を介さないので動作が早い
  • デメリット
    • py32 と比べて対応ライブラリが少ない
    • win32 で動作しない

したがって(多少苦労してでも)巨大データの取り扱いやパフォーマンスを欲するなら py64 を使い、そうでなければ py32 を使うのが無難と言えるだろう。

最後に、python ライブラリについても 32 ビットと 64 ビットがあるが、これはpython のビット数と同じものを使う(Windowsのビット数じゃないよ)。py32 を使ってるならライブラリも32ビット、py64を使ってるならライブラリも64ビット

結論ここまで。以降は細かい話。

トピック1: そもそも32ビットと64ビットって何が違うの?

ビット数は性能みたいなもの。めっちゃ噛み砕いて言うなら手の本数。手が32本しかないよりも64本あった方が、作業は圧倒的にはかどる。

歴史的には8本や16本もあったけど、長らく32本(32ビットWindows)が続いていた。けれど技術革新が進み、64本もいけるようになってきた。そこでWindowsも64本に対応し始めた。それが64ビットWindows

これにより、64ビットWindows上で64ビット対応アプリケーションをつくれば、そのアプリは64本の手を使って、より効率的に動作する。64ビットPythonもその内の一つ。

これでめでたしかというと、そうじゃない。手の本数が違うと根本的に仕組みが違うので、64ビットWindowsでは従来のアプリケーション(32ビットアプリケーション)が動かない。互換性が無いとも言いますね。さあ困ったぞ、どうする?

トピック2: WOW64

WOW64(Windows on Windows 64)は、64ビットWindowsで32ビットWindowsを動かすための仕組み。エミュレータとも呼ばれる。内部はよーわからんが、なんか色々上手いことやって動かせるようにしている。

だから64ビットWindowsでも32ビットアプリケーションが動く。ただし内部で色々やっている分、動作が遅い

ちなみに64ビットWindows上に32ビットアプリケーションをインストールすると C:\Program files(x86) という場所に格納される。Program files フォルダが二つあって違いがわからんって思ってたけど、

  • C:\Program files → 64ビットアプリを格納
  • C:\Program files(x86) →32ビットアプリを格納

こういう意味なんですね。

参考