挨拶
ステップイン・ステップアウトをやる方法を考えてて、そういや俺Pythonでランチャとか作ってたよなと思って試しに作ってみる。案外すぐにそれっぽいのができた。
というわけで、今回はPython + wxPythonでステップイン・ステップアウトのみが出来る旧倉っぽいランチャを作ってみる。
今までは、他のソフトをカスタマイズするとかスクリプトを作るとか組み合わせるとかしか選択肢がなかったけど、「作る」コマンドが状況によっては浮かぶようになったのかもしれにゃい。
準備
とりあえずは環境を整える。
Python
wxPython
Python Win32 Extensions
をダウンロードし、インストール。
適当なフォルダを作り、そこに以下の二つのファイルを保存。homu_ini.pyはCraftLaunchのソースを参考にした
homuLauncher.py
# -*- encoding: utf-8 -*-
import wx,subprocess,os,sys,re,win32file,ConfigParser
import homu_ini
os.chdir(os.path.dirname(sys.argv[0]) or '.')
class MyApp(wx.PySimpleApp):
def OnInit(self):
homu_ini.read()
# 引数の取得
argvs = sys.argv
argc = len(argvs)
# 引数を連結する
argdir = os.path.abspath(' '.join(argvs[1:]))
start_folder = homu_ini.get( "MAIN", "start_folder", "" )
print start_folder
if argc >= 2 and os.path.isdir(argdir):
# 引数があればそのフォルダを起動ディレクトリにする
self.CurrentDir = argdir
elif os.path.isdir(start_folder):
self.CurrentDir = start_folder
else:
self.CurrentDir = os.path.abspath(os.path.dirname(sys.argv[0]))
# 前回終了時の位置を復元
if homu_ini.getint( "MAIN", "HistoryMode", 1 ) == 1:
self.x = homu_ini.getint( "HISTORY", "X", 300 )
self.y = homu_ini.getint( "HISTORY", "Y", 300 )
# デフォルトの位置
else:
self.x = homu_ini.getint( "MAIN", "X", 300 )
self.y = homu_ini.getint( "MAIN", "Y", 300 )
# インクリメンタルサーチ用の変数
self.typedtext = ""
self.lbox_list = []
# メインウィンドウを作成
self.Frm = wx.Frame(None, -1, "homuLauncher",style=wx.DOUBLE_BORDER|wx.FRAME_NO_TASKBAR)
# 位置を指定
pos = (self.x, self.y)
self.Frm.SetPosition(pos)
self.TxtCtr = wx.TextCtrl(self.Frm, -1)
self.Frm.Bind(wx.EVT_MOVE,self.onMove)
self.TxtCtr.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.TxtCtr.Bind(wx.EVT_CHAR,self.OnKeyChar)
self.tb_sizer = wx.BoxSizer(wx.VERTICAL)
self.tb_sizer.Add(self.TxtCtr, 1, wx.EXPAND)
self.Frm.SetSizer(self.tb_sizer)
# リストウィンドウを作成
self.lbFrame = wx.Frame(None, 0, "wxPython",style=wx.DOUBLE_BORDER|wx.FRAME_NO_TASKBAR)
self.LBox = wx.ListBox(self.lbFrame, -1)
self.lb_sizer = wx.BoxSizer(wx.VERTICAL)
self.lb_sizer.Add(self.LBox, 1, wx.EXPAND)
self.lbFrame.SetSizer(self.lb_sizer)
# サブディレクトリのリストを取得し、リストボックスにセット
self.LBox.SetItems(self.GetSubDirList(self.CurrentDir))
# メインウィンドウのサイザーをセット
self.Frm.SetAutoLayout(True)
self.tb_sizer.Fit(self.Frm)
# メインウィンドウの大きさを調整
self.MainWindowSetSize(self.AddSlash(self.CurrentDir))
# テキストボックスにパスを入力
self.TxtCtr.SetValue(self.AddSlash(self.CurrentDir))
# リストウィンドウの大きさを調整
self.ListWindowSetSize()
self.Frm.Show()
if self.LBox.GetCount() != 0:
self.lbFrame.Show()
self.TxtCtr.SetInsertionPoint(-1)
self.TxtCtr.SetFocus()
return 1
def OnKeyDown(self,event):
key = event.GetKeyCode()
if key == wx.WXK_ESCAPE:
self.OnExit()
elif key == wx.WXK_UP:
count = self.LBox.GetCount()
if count == 0:
return
next = self.LBox.GetSelection() - 1
if next >= 0:
self.LBox.SetSelection(next)
else: self.LBox.SetSelection(count - 1)
hoge = os.path.join(self.CurrentDir,self.LBox.GetStringSelection())
# メインウィンドウの大きさを調整
self.MainWindowSetSize(hoge)
# テキストボックスにパスを入力
self.TxtCtr.SetValue(hoge)
# テキストボックスの選択範囲を調整
self.TxtCtr.SetSelection(len(self.AddSlash(self.CurrentDir)),-1)
elif key == wx.WXK_DOWN:
count = self.LBox.GetCount()
if count == 0:
return
next = self.LBox.GetSelection() + 1
if next < count:
self.LBox.SetSelection(next)
else: self.LBox.SetSelection(0)
hoge = os.path.join(self.CurrentDir,self.LBox.GetStringSelection())
# メインウィンドウの大きさを調整
self.MainWindowSetSize(hoge)
# テキストボックスにパスを入力
self.TxtCtr.SetValue(hoge)
# テキストボックスの選択範囲を調整
self.TxtCtr.SetSelection(len(self.AddSlash(self.CurrentDir)),-1)
elif key == wx.WXK_RIGHT:
if self.typedtext == "":
# リストボックスが空なら終了
if self.LBox.GetSelection() == -1:
return
hoge = os.path.join(self.CurrentDir,self.LBox.GetStringSelection())
fuga = self.GetSubDirList(hoge)
# サブディレクトリがなければ終了
if fuga == []:
return
# リストボックスにサブディレクトリのリストをセット
self.LBox.SetItems(fuga)
# メインウィンドウの大きさを調整
self.MainWindowSetSize(self.AddSlash(hoge))
# テキストボックスにパスを入力
self.TxtCtr.SetValue(self.AddSlash(hoge))
# テキストボックスの選択範囲を調整
self.TxtCtr.SetInsertionPoint(-1)
self.ListWindowSetSize()
self.CurrentDir = hoge
else:
hoge = os.path.join(self.CurrentDir,self.LBox.GetStringSelection())
fuga = self.GetSubDirList(hoge)
# サブディレクトリがなければ終了
if fuga == []:
return
self.typedtext = ""
# リストボックスにサブディレクトリのリストをセット
self.LBox.SetItems(fuga)
# メインウィンドウの大きさを調整
self.MainWindowSetSize(self.AddSlash(hoge))
# テキストボックスにパスを入力
self.TxtCtr.SetValue(self.AddSlash(hoge))
# テキストボックスの選択範囲を調整
self.TxtCtr.SetInsertionPoint(-1)
self.ListWindowSetSize()
self.CurrentDir = hoge
elif key == wx.WXK_LEFT:
if self.typedtext == "":
# ルートフォルダなら何もしない
if self.CurrentDir == os.path.dirname(os.path.abspath(self.CurrentDir)):
return
self.CurrentDir = os.path.dirname(os.path.abspath(self.CurrentDir))
# メインウィンドウの大きさを調整
self.MainWindowSetSize(self.AddSlash(self.CurrentDir))
# テキストボックスにパスを入力
self.TxtCtr.SetValue(self.AddSlash(self.CurrentDir))
# テキストボックスの選択範囲を調整
self.TxtCtr.SetInsertionPoint(-1)
self.LBox.SetItems(self.GetSubDirList(self.CurrentDir))
self.ListWindowSetSize()
if not self.lbFrame.IsShown():
self.lbFrame.Show()
self.TxtCtr.SetFocus()
else:
self.typedtext = ""
hoge = self.AddSlash(self.CurrentDir)
# メインウィンドウのサイズを調整
self.MainWindowSetSize(hoge)
# エディットボックスに入力
self.TxtCtr.SetValue(hoge)
self.TxtCtr.SetInsertionPoint(-1)
self.LBox.SetItems(self.GetSubDirList(hoge))
self.ListWindowSetSize()
self.lbox_list = []
elif key in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
d = {"path": "\"" + self.TxtCtr.GetValue().encode("shift_jis") + "\""}
hoge = self.TxtCtr.GetValue().encode("shift_jis")
if os.path.isdir(hoge):
folder_cmd = homu_ini.get( "MAIN", "open_folder_command", "" ).format(**d).strip()
if folder_cmd != "":
subprocess.Popen(folder_cmd)
else:
subprocess.Popen('explorer \"' + hoge + '\"' )
elif os.path.isfile(hoge):
file_cmd = homu_ini.get( "MAIN", "open_file_command", "" ).format(**d).strip()
if file_cmd != "":
subprocess.Popen(file_cmd)
else:
subprocess.Popen('explorer \"' + hoge + '\"' )
self.OnExit()
return
else: event.Skip()
def OnKeyChar(self,event):
key = event.GetKeyCode()
# BACKSPACEで一文字消去
if key == wx.WXK_BACK:
word = self.typedtext[:-1]
if self.typedtext == "":
# ルートフォルダなら何もしない
if self.CurrentDir == os.path.dirname(os.path.abspath(self.CurrentDir)):
return
self.CurrentDir = os.path.dirname(os.path.abspath(self.CurrentDir))
# メインウィンドウの大きさを調整
self.MainWindowSetSize(self.AddSlash(self.CurrentDir))
# テキストボックスにパスを入力
self.TxtCtr.SetValue(self.AddSlash(self.CurrentDir))
# テキストボックスの選択範囲を調整
self.TxtCtr.SetInsertionPoint(-1)
self.LBox.SetItems(self.GetSubDirList(self.CurrentDir))
self.ListWindowSetSize()
if not self.lbFrame.IsShown():
self.lbFrame.Show()
self.TxtCtr.SetFocus()
elif word == "":
self.typedtext = word
hoge = self.AddSlash(self.CurrentDir)
# メインウィンドウのサイズを調整
self.MainWindowSetSize(hoge)
# エディットボックスに入力
self.TxtCtr.SetValue(hoge)
self.TxtCtr.SetInsertionPoint(-1)
self.LBox.SetItems(self.GetSubDirList(hoge))
self.ListWindowSetSize()
self.lbox_list = []
else:
isearch = IncrementalSearch(word,self.GetSubDirList(self.CurrentDir))
try:
new_list = isearch.GetBackList()
except:
return False
if new_list != self.lbox_list:
# リストをセットして大きさを調整
self.LBox.SetItems(new_list)
self.ListWindowSetSize()
self.LBox.SetSelection(0)
# エディットボックスとリストボックスからパスを生成
hoge = os.path.join(self.CurrentDir,new_list[0])
# メインウィンドウのサイズを調整
self.MainWindowSetSize(hoge)
self.TxtCtr.SetValue(hoge)
self.TxtCtr.SetSelection(len(self.CurrentDir + word) , -1)
self.typedtext = word
self.lbox_list = new_list
else:
self.TxtCtr.SetSelection(len(self.CurrentDir + word) , -1)
self.typedtext = word
# 英数字で前方一致検索
elif key >= 32 and key <= 127:
word = self.typedtext + chr(key)
isearch = IncrementalSearch(word,self.GetSubDirList(self.CurrentDir))
try:
new_list = isearch.GetBackList()
except:
return False
if new_list == []:
return
if new_list != self.lbox_list:
# リストをセット
self.LBox.SetItems(new_list)
self.LBox.SetSelection(0)
# パスを生成
hoge = os.path.join(self.CurrentDir,new_list[0])
# メインウィンドウのサイズを調整
self.MainWindowSetSize(hoge)
self.TxtCtr.SetValue(hoge)
self.TxtCtr.SetSelection(len(self.AddSlash(self.CurrentDir) + word) , -1)
self.typedtext = word
self.lbox_list = new_list
self.ListWindowSetSize()
else:
self.TxtCtr.SetSelection(len(self.AddSlash(self.CurrentDir) + word) , -1)
self.typedtext = word
else: pass
return
def GetSubDirList(self,fp):
lookup = ('utf_8', 'euc_jp', 'euc_jis_2004', 'euc_jisx0213',
'shift_jis', 'shift_jis_2004','shift_jisx0213',
'iso2022jp', 'iso2022_jp_1', 'iso2022_jp_2', 'iso2022_jp_3',
'iso2022_jp_ext','latin_1', 'ascii')
for encoding in lookup:
try:
fp = fp.decode(encoding)
break
except:
pass
if os.path.isfile(fp):
return []
SubDirList = []
SubFileList = []
file_list = os.listdir(fp)
for file_name in file_list:
x = os.path.join(fp,file_name)
# 隠しフォルダは除外
attrib = win32file.GetFileAttributesEx(x)
if (attrib[0] & win32file.FILE_ATTRIBUTE_HIDDEN) == 0:
# フォルダをリストに追加
if os.path.isdir(x):
SubDirList.append(x)
# ファイルをリストに追加
if os.path.isfile(x) and homu_ini.getint( "MAIN", "ShowFile", 0 ) == 1:
SubFileList.append(x)
return map((lambda x: os.path.basename(x) + "\\"),SubDirList) + map((lambda x: os.path.basename(x)),SubFileList)
def ListWindowSetSize(self):
# 位置
self.lb_sizer.Fit(self.lbFrame)
pos = (self.Frm.GetPosition().x + self.Frm.GetTextExtent(self.TxtCtr.GetValue())[0], self.Frm.GetPosition().y + self.Frm.GetSize().y)
# 大きさ。リスト数が10を超えたら調整
if self.LBox.GetCount() >= 10:
lbox_y = self.Frm.GetTextExtent(self.TxtCtr.GetValue())[1] * 10
else:
lbox_y = self.Frm.GetTextExtent(self.TxtCtr.GetValue())[1] * (self.LBox.GetCount() +1)
size = (self.lbFrame.GetSize().x, lbox_y )
# 位置と大きさをセット
self.lbFrame.SetDimensions(pos[0],pos[1],size[0],size[1])
def MainWindowSetSize(self,text):
#self.tb_sizer.Fit(self.Frm)
size = (self.Frm.GetTextExtent(text)[0] + homu_ini.getint( "MAIN", "MainWindowWidthAdd", 22 ), self.Frm.GetSize().y)
self.Frm.SetSize(size)
# 末尾に\をつける
def AddSlash(self,word):
hoge = os.path.abspath(word)
if hoge.endswith("\\"):
return hoge
else:
return hoge + '\\'
def SearchWord(self,word):
new_list = []
for line in self.List:
if word in line:
new_list.append(line)
return new_list
def onMove(self,event):
self.ListWindowSetSize()
# iniファイルへの書き込み
def WriteINI(self):
homu_ini.set('MAIN','HistoryMode',str(homu_ini.getint( "MAIN", "HistoryMode", 1 )))
homu_ini.set('MAIN','x',str(homu_ini.getint( "MAIN", "x", 300 )))
homu_ini.set('MAIN','y',str(homu_ini.getint( "MAIN", "y", 300 )))
homu_ini.set('MAIN','showfile',str(homu_ini.getint( "MAIN", "showfile", 0 )))
homu_ini.set('MAIN','MainWindowWidthAdd',str(homu_ini.getint( "MAIN", "MainWindowWidthAdd", 22 )))
homu_ini.set('MAIN','open_folder_command',homu_ini.get( "MAIN", "open_folder_command", "" ))
homu_ini.set('MAIN','open_file_command',homu_ini.get( "MAIN", "open_file_command", "" ))
homu_ini.set('MAIN','start_folder',homu_ini.get( "MAIN", "start_folder", "" ))
if homu_ini.getint( "MAIN", "HistoryMode", 1 ) == 1:
homu_ini.set('HISTORY','X',str(self.Frm.GetPosition()[0]))
homu_ini.set('HISTORY','Y',str(self.Frm.GetPosition()[1]))
homu_ini.write()
def OnExit(self):
# iniファイルへの書き込み
self.WriteINI()
wx.Exit()
class IncrementalSearch:
def __init__(self,pattern,mylist):
self.pattern = pattern
self.new_list = []
self.old_list = mylist
def GetBackList(self):
for x in self.old_list:
if x.lower().find(self.pattern.lower()) == 0:
self.new_list.append(x)
return self.new_list
app = MyApp()
app.MainLoop()
homu_ini.py
# -*- encoding: utf-8 -*-
import os
import sys
import ConfigParser
ini = None
ini_filename = "homu.ini"
#--------------------------------------------------------------------
def read():
global ini
ini = ConfigParser.RawConfigParser()
try:
ini.read(ini_filename)
except:
pass
def write():
try:
f = open(ini_filename,"w")
ini.write(f)
f.close()
except:
pass
def get( section, option, default=None ):
#print "ini.get", section, option
try:
return ini.get( section, option )
except:
if default!=None:
return default
raise
def getint( section, option, default=None ):
#print "ini.getint", section, option
try:
return ini.getint( section, option )
except:
if default!=None:
return default
raise
def set( section, option, value ):
#print "ini.set", section, option, value
assert( type(value)==str )
try:
if ini.get(section,option)==value:
return
except:
pass
try:
ini.add_section(section)
except ConfigParser.DuplicateSectionError:
pass
ini.set( section, option, value )
def setint( section, option, value ):
#print "ini.setint", section, option, value
assert( type(value)==int )
try:
if ini.getint(section,option)==value:
return
except:
pass
try:
ini.add_section(section)
except ConfigParser.DuplicateSectionError:
pass
ini.set( section, option, str(value) )
def items( section, default=None):
#print "ini.get", section, option
try:
return ini.items( section )
except:
if default!=None:
return default
raise
homuLauncher.pyを実行してみよう。それで
こんなウィンドウがでたら成功です。カーソルキーで移動、Escで終了、Enterで実行です。あと前方一致で自動補完が効きます。
起動時のパスは、引数を与えることで指定可能。
コマンドプロンプトを出したくない場合は、homuLauncher.pyをhomuLauncher.pywにリネームしましょう。
設定ファイル
一度起動し、Escを押すなどして終了するとhomu.iniというファイルができるはず。そこでファイラなどを指定する。僕の場合だと次のようになる。
[MAIN]
historymode = 1
x = 300
y = 300
showfile = 1
mainwindowwidthadd = 22
open_folder_command = D:\bin\ppx\PPCW.EXE -r {path}
open_file_command = D:\bin\esExt\esExt5.exe {path}
start_folder = D:\bin
[HISTORY]
x = 300
y = 300
open_folder_commandでフォルダを開くファイラを指定。{path}はhomuLauncherで選択したパスを意味する。
open_file_commandはファイルを開くプログラムを指定。{path}はhomuLauncherで選択したパスを意味する。
start_folderは起動時のフォルダ。引数を指定しなければこれになる。
showfileはファイルを表示するかどうか。1なら表示、0なら非表示。
historymodeは場所や大きさを記憶するかどうか。1なら記憶、0なら記憶しない。
mainwindowwidthaddは、ここで指定した数値だけメインウィンドウの幅を伸ばす
PPxへの登録
以下のようなコマンドを登録することで、カレントフォルダを起動フォルダにしてほむランチャを開くことができる。
%Obi D:\Work\Python\homuLauncher.py %1
位置を調整したいときは、拡張子をpywに変え、PPXWIN.DLLとPPXMES.DLLをインストールして
%Obi D:\Work\Python\homuLauncher.pyw %1 %: *fitwindow %NC,%*findwindowtitle("homuLauncher"),20
のようなコマンドに。