Pythonでコマンドラインランチャを作ろう!第七回−自動補完

前置き

今日やるのは自動補完です。
craftlaunchである、あれですね。では、ちょっと見てみましょうか^^


こんな感じですね。まあ、自動補完は一般的っぽいので説明はいいでしょう( ^ω^)♪

こういうふうにやったらいいんじゃないだろうか

  1. テキストコントロールに打ち込まれた文字を取得する
  2. それで、表示したいコマンドのリストを検索する
  3. 検索結果で一番上にきたのをテキストコントロールにセットする
  4. テキストコントロールの文字列のうち、打ち込んでいないものを選択状態にする

では順番にいきましょー

テキストコントロールに打ち込まれた文字の取得

では、はじめに本体から。テキストコントロールを使い、それをBindでOnKeyCharという関数と結びつけているわけですね。

class MyApp(wx.PySimpleApp):
    def OnInit(self):
        CMD_FILE = "data/instant.txt"
        self.Dict = {}
        f = open(CMD_FILE, "r")
        for x in f.readlines():
            line = x.split("=")
            key = line[0].strip()
            path = line[1].strip()
            self.Dict[key] = path
        f.close()
        self.typedtext = ""
        Frm = wx.Frame(None, -1, "wxPython", size=(200,45),pos = (400,600))
        self.TxtCtr = wx.TextCtrl(Frm, -1)
        self.TxtCtr.Bind(wx.EVT_CHAR,self.OnKeyChar)
        Frm.Show()
        return 1

    def OnKeyChar(self,event):
        key = event.GetKeyCode()
        if key >= 32 and key <= 127:
            self.TxtCtr.Clear()
            self.typedtext = self.typedtext + chr(key)
            wordcount = len(self.typedtext)
            itemlist = self.SearchList(self.typedtext)
            if itemlist != -1:
                item = itemlist[0]
                self.TxtCtr.SetValue(item)
                self.TxtCtr.SetSelection(wordcount , -1)
            else: pass
            return

前回のinstant.txtを利用しています。3行目のCMD_FILE = "data/instant.txt"以下のところですね。

関数との結びつけには、EVT_CHARを使います。
これは、テキストコントロールに何かのキーが打ち込まれたらイベントを起こすもの。
EVT_TEXTとの違いは、英数字以外でも反応することと、イベントを発生した文字自体は取得できないことでしょうか。だから、以前EVT_TEXTでやったようなのと同じことを、これでやろうとすると少し面倒になる。

if 97 <= event.GetKeyCode() <=122 or 48<= event.GetKeyCode() <= 57:

として、英数字だけに反応するように分岐させたり

txt_word = str(self.TxtCtr.GetValue()) + chr (event.GetKeyCode())

として、テキストコントロールに打ち込まれた文字と、EVT_CHARを発生させたキーとを結合する必要があったりする。
何でこんなことを知っているかというと、僕が最初EVT_TEXTの存在を知らずEVT_CHARでランチャを作ろうとしていたからです^^;

では、なぜ今回これを使うかというとEVT_TEXTを使えないからなんですね。テキストコントロールには補完した文字列を表示しないといけないからです。
今回、テキストコントロールの四角いボックス自体はそこから何か取得できるとは考えてはいけない。それは、ただ処理した結果を出力する場所でしかないわけですにゃ。

event.GetKeyCode()と打ち込んだ文字の取得

というわけで、EVT_CHARを使うわけです。event.GetKeyCode()で、打ち込んだキーを取得するわけですね。OnKeyCharの2行目で、keyに代入しています。
では、二文字目以降打ち込んだ時はどうやって取得するのか、というと次のようにします。
まず、最初にOnIniで次のような定義をします。

typedtext = ""

ついで、EVT_CHARと結びつけたOnKeyCharで次のようにします。5行目ですね。

typedtext = typedtext + event.GetKeyCode()

おわかりですね。typedtextに、打ち込んだ文字を次々と保存していくわけです。wxPythonのデモにこのやり方がありました。
一文字目を打ち込んだときには、当然typedtextは空なんだからtypedtextには、打ち込んだキーが保存される。二文字目を打ち込んだときには、typedtextには一文字目が入っているのだから、typedtext = typedtext + event.GetKeyCode()は一文字目と二文字目を足したものになる。というわけです。☆ミ
ちなみに、今回の段階では一度入力した文字を消すことができません^^;
あくまで試作品ということでご容赦を^^;

表示したいコマンドのリストを検索

    def SearchList(self,word):
        boxlist = []
        word = word.lower()
        length = len(word)
        for x in self.Dict.keys():
            x = x.lower()
            if x[:length] == word:
                boxlist.append(x)
        if boxlist ==[]:
            return -1
        return boxlist

コマンドのリストは、以前作成した辞書を利用しましょう。self.Dict.keys()のあれですね。
で、文字列を渡されたらそのリストを検索し、結果をリストにして返す関数を作る・・・ってこれも前回までと同じでいいのか。
いちおう、前方一致にしたほうがそれっぽいので前方一致するように、少し書き換えてます。

検索結果で一番上にきたのをテキストコントロールにセット

    def OnKeyChar(self,event):
        key = event.GetKeyCode()
        if key >= 32 and key <= 127:
            self.TxtCtr.Clear()
            self.typedtext = self.typedtext + chr(key)
            wordcount = len(self.typedtext)
            itemlist = self.SearchList(self.typedtext)
            if itemlist != -1:
                item = itemlist[0]
                self.TxtCtr.SetValue(item)
                self.TxtCtr.SetSelection(wordcount , -1)
            else: pass
            return

OnKeyCharの、下半分でこれをやっています。検索結果で一番上にきたのをテキストコントロールにセットしたいわけですが、これでセットするのは検索結果で一番上にきたもの、つまり一番目の要素ですね。
下から5行目の

itemlist[0]

で取得できます。
これのテキストコントロールへのセットには、SetValueを使います。前にも使いましたね。

打ち込んでいないものを選択状態に

選択状態にするには、SetSelectionを使います。OnKeyCharの下から三行目ですね。

上から6行目で、wordcountに、typedtextすなわち今まで打ち込んだ文字の数を数えたものを代入。
SetSelection(wordcount,-1)
としたとき、二つの引数の一つめが選択範囲の左を、二つめが選択範囲の右端を意味する。
とりあえず、打ち込んだ文字以外が選択されることになるわけですね。

まとめとか

次回も自動補完の続きです。テキストコントロールに表示されなかったリストを、リストボックスとして表示するのをやります。一番上のcraftlaunchの画像で言えば二つめで表示されている、あれですね。

何で、基本編終わったとかいってたのに自動補完なんてやっているのか。最初は僕もやる気はなかったんです。ランチャを作るとしたら、きっとfenrir型になるだろうし別にcraftlaunch型とかいいんじゃね?結構そういう系のランチャってたくさんありそうだし、なーんて。
でも、どういうのをつくろっかな、fenrirと全く同じの作っても別におもしろくないしなとか考えてて、いろいろうろうろしてどの方向にいこっかなーと思っていたとき、実はfenrir型とcraftlaunch型とを組み合わせたものとかないんじゃね、これ作るだけでよくね、と思いついたわけです。数年前に某巨大掲示板界隈で議論されていた、fenrirのインスタントコマンドの自動補完云々というやつですな。


こんなことが

がんばれば

できるようになるんじゃ

ないか・・・と(=ω=.)

exp04

import wx


class MyApp(wx.PySimpleApp):
    def OnInit(self):
        CMD_FILE = "data/instant.txt"
        self.Dict = {}
        f = open(CMD_FILE, "r")
        for x in f.readlines():
            line = x.split("=")
            key = line[0].strip()
            path = line[1].strip()
            self.Dict[key] = path
        f.close()
        self.typedtext = ""
        Frm = wx.Frame(None, -1, "wxPython", size=(200,45),pos = (400,600))
        self.TxtCtr = wx.TextCtrl(Frm, -1)
        self.TxtCtr.Bind(wx.EVT_CHAR,self.OnKeyChar)
        Frm.Show()
        return 1



    def OnKeyChar(self,event):
        key = event.GetKeyCode()
        if key >= 32 and key <= 127:
            self.TxtCtr.Clear()
            self.typedtext = self.typedtext + chr(key)
            wordcount = len(self.typedtext)
            itemlist = self.SearchList(self.typedtext)
            if itemlist != -1:
                item = itemlist[0]
                self.TxtCtr.SetValue(item)
                self.TxtCtr.SetSelection(wordcount , -1)
            else: pass
            return

    def SearchList(self,word):
        boxlist = []
        word = word.lower()
        length = len(word)
        for x in self.Dict.keys():
            x = x.lower()
            if x[:length] == word:
                boxlist.append(x)
        if boxlist ==[]:
            return -1
        return boxlist

app = MyApp()
app.MainLoop()