久しぶりにWindowsプログラム
事情があって、ウン十年ぶりにWindowsプログラムをつくることになった。できるだけトレンディを身につけたく調べていたところ、Python+tkinterでWindowsプログラムを実現できることがわかったので、その記録をここに残すこととした。小生、Pythonは初心者レベル、tkinterは初めてのトライであり、基本的なことも記載。
tkinter
Python上でGUIを実現するもの(ライブラリ)。山のように情報が見つかる。
やること
sshでLinuxにログインし、"ls"にてホームディレクトリのファイルやディレクトリの一覧を取得し、"ls -ld"にて選択したファイルまたはディレクトリの詳細を表示するもの。
内容にあまり意味はなく、作成しながら学びたいこと(下記内容)を積み上げたら、このようなプログラムとなった。
Python
- 関数
- ログ
- 例外処理
- 文字列操作
- ssh利用
など。
tkinter
- GUI全般
- メニュー
- コンボボックス
- テキストボックス
- ボタン
- EXE化
など。
ソースコード+α
関数の関係で定義された変数が先に記載されたり、わかりにくいところがあるが、1行目から順に解説。
from tkinter import *
from tkinter import ttk
import paramiko
import socket
import logging
import logging.handlers
from datetime import datetime
各種ライブラリのインポートで。tkinter、ログ、日付など。paramikoがssh用ライブラリで、端的な説明として、「PythonでSSH接続する方法【初心者向け】」を参考にした。これがあれば、socketは不要と思われたが、後述する接続エラー検出時にこれが必要となった。
import warnings
warnings.filterwarnings('ignore')
Warningメッセージが処理する際に悪さをしていたため、上記により削除。「Pythonで実行時のwarningが出ないようにする」を参考。
### Size and Position
WIN_S = '400x350'
X_POS = 50
X_W = 100
Y_POS = 10
Y_H = 40
X_MSG = 100
Y_MSG = 300
MSG_W = 200
RES_W = 400
Windowサイズや表示位置の定義。
### Get IP address, ID and Password from Text box
def get_conn_info():
return srvbox.get(), usrbox.get(), pwdbox.get()
後述するテキストボックス(IPアドレス、ユーザ名、パスワード)の入力値を取得する関数。return文に複数の戻り値を記載できることが、ろうとるには新鮮だった。
### Connect to server
def connect_srv():
log.debug("== connect_srv ==")
dst, id, key = get_conn_info()
log.debug(f" IP address:{dst} User:{id} Password:{key}")
try:
cli.connect(dst, username=id, password=key, timeout=3)
except socket.timeout: # paramiko.SSHException does not work
msg = Label(root, text="Connection Error", fg='red')
msg.place(x=X_MSG, y=Y_MSG, width=MSG_W)
log.debug(' Connection Error')
return 1
except paramiko.AuthenticationException:
msg = Label(root, text="Authentication Error", fg='red')
msg.place(x=X_MSG, y=Y_MSG, width=MSG_W)
log.debug(' Authentication Error')
cli.close()
return 1
except Exception:
msg = Label(root, text="Other Errors", fg='red')
msg.place(x=X_MSG, y=Y_MSG, width=MSG_W)
log.debug(' Other Errors')
cli.close()
return 1
msg = Label(root, text="Connected")
msg.place(x=X_MSG, y=Y_MSG, width=200)
log.debug(' Connected')
return 0
sshサーバーへの接続関数。ログ化(log.debug())や接続(cli.connect())実行前に、変数logおよびcliの定義が必要であるが、これらは後述するメイン部分で記載されている。
- IPアドレス、ユーザ名、パスワード取得
- サーバーへの接続および成功時の表示(Label)およびログ化
- エラー時の処理(try - except)
- 接続エラー(各種サイトでよく見つかるparamiko.SSHExceptionではこのエラーを検出できず、大元のsocket.timeoutを利用)の表示およびログ化
- 認証エラー(paramiko.AuthenticationException)の表示およびログ化
- その他のエラー(Exception)の表示およびログ化
- 成功時は”0”、エラー時は”1”を返す
下記はエラー表示の例である。
### In case of "ls" click
def ls_clk():
log.debug("==== ls_clk ====")
ret = connect_srv()
if ret != 0:
return
stdin, stdout, stderr = cli.exec_command('ls')
data = []
for line in stdout: # stdout.read() does not work
data.append(line.splitlines()) # Also only 'line' works.
#line.rstrip('\n') # does not work for removal of LF
for item in line.splitlines(): # for log
log.debug(' ' + item)
cbox['values'] = data # Getting data is listed in combo box
cbox.current(0) # 1st index
cli.close()
”ls”ボタンクリック時にコールされる関数。サーバーへの接続後、”ls”実行結果(stdout)を行単位に処理し(splitlines()で改行コードの削除)、取得したファイル名またはディレクトリ名を、後述するコンボボックス(cbox)のリストに代入する(cbox['values'])。コンボボックスのリスト内容を、何かのきっかけにより更新する方法は、「How To Create a Search-able Combo Box | Tkinter and Python | How To Create a Combo Box」を参考にした。
### In case of "ls -ld" click
def lsld_clk():
log.debug("==== lsld_clk ====")
item = cbox.get()
if len(item) == 0:
msg = Label(root, text="No selected file", fg='red')
msg.place(x=X_MSG, y=Y_MSG, width=MSG_W)
log.debug(' No selected file')
return
ret = connect_srv()
if ret != 0:
return
stdin, stdout, stderr = cli.exec_command(f'ls -ld {item}')
result = stdout.read() # .read() is necessary.
log.debug(' ' + result.decode())
#log.debug(result) # outputs b''
s = Label(root, text=result)
s.place(y=Y_POS+6*Y_H, width=RES_W) # width is necessary to override previous result
cli.close()
”ls -ld”ボタンクリック時にコールされる関数。コンボボックスにて選択されている要素を取得し、サーバーへの接続後、”ls -ld”を実行し、Labelにて表示する。なお、コンボボックスにて選択されていないときは、エラー表示する。
### For debug logging
def debug_tgl():
tgl.set(not tgl.get())
if tgl.get():
log.setLevel(logging.DEBUG)
fh = logging.FileHandler('{:Log-%Y%m%d-%H%M%S}.log'.format(datetime.now()))
log.addHandler(fh)
else:
log.setLevel(logging.INFO)
後述する”Debug”メニュー選択時にコールされる関数。フラグ(Boolean:tgl)ON(TRUE)時、ログレベルをDEBUGにし、日時を含んだログファイルを作成する。OFF(FALSE)にはログレベルをINFOにする。本プログラムでは、log.debug()しか使われていないため、OFF時にはログなしとなる。
##### Main #####
# Log
log = logging.getLogger(__name__)
# Window
root = Tk()
root.title("ls by ssh")
root.geometry(WIN_S)
# Paramiko for ssh connection
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.WarningPolicy())
メイン部分。ログ初期化、Windows作成、ssh接続初期化。
# Menu
menubar = Menu(root)
filemenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="Exit", command=root.quit) # only 'quit' may not work
tgl = BooleanVar() # toggle for log
tgl.set(FALSE)
filemenu.add_checkbutton(label="Debug", command=debug_tgl)
root.config(menu=menubar)
メニューバー作成およびログ用フラグ(tgl)の初期化。ログ用メニュー”Debug”はトグルとなっており、選択(クリック)ごとにチェックが付加されたり消えたりする。”Exit”クリック時にはプログラムが終了。
# Input item (Text box)
srv = Label(root, text='IP address')
srv.place(x=X_POS, y=Y_POS)
srvbox = Entry(root)
srvbox.place(x=X_POS+X_W, y=Y_POS)
usr = Label(root, text='User name')
usr.place(x=X_POS, y=Y_POS+Y_H)
usrbox = Entry(root)
usrbox.place(x=X_POS+X_W, y=Y_POS+Y_H)
pwd = Label(root, text='Password')
pwd.place(x=X_POS, y=Y_POS+2*Y_H)
pwdbox = Entry(root, show='*')
pwdbox.place(x=X_POS+X_W, y=Y_POS+2*Y_H)
IPアドレス、ユーザ名、パスワード取得用のテキストボックス。パスワードに関しては、入力内容が見えないよう”show='*'”を利用。
# Button for ls
btn1 = Button(root, text='ls', command=ls_clk)
btn1.place(x=X_POS+X_W, y=Y_POS+3*Y_H)
# Box
cbox = ttk.Combobox(root, values=[])
cbox.place(x=X_POS+X_W, y=Y_POS+4*Y_H)
#cbox.bind("<<ComboboxSelected>>", sel_val)
# Button for ls -ld
btn2 = Button(root, text='ls -ld selected file', command=lsld_clk)
btn2.place(x=X_POS+X_W, y=Y_POS+5*Y_H)
# Loop
root.mainloop()
ボタン、コンボボックス、メインループ。ボタン、コンボボックス選択時のコールする関数を”command”で指定する。
ログファイルサンプル
Debug ON時のログファイルのサンプルである。
==== ls_clk ====
== connect_srv ==
IP address:192.168.250.9 User:test Password:himitsu
Connection Error
==== ls_clk ====
== connect_srv ==
IP address:192.168.250.1 User:test Password:himitu
Authentication Error
==== ls_clk ====
== connect_srv ==
IP address:192.168.250.1 User:test Password:himitsu
Connected
Desktop
Documents
Downloads
Library
Movies
Music
Pictures
Public
==== lsld_clk ====
== connect_srv ==
IP address:192.168.250.1 User:test Password:himitsu
Connected
drwx------+ 3 test staff 102 Mar 17 22:20 Downloads
==== lsld_clk ====
== connect_srv ==
IP address:192.168.250.1 User:test Password:himitsu
Connected
drwx------+ 3 test staff 102 Mar 17 22:20 Music
PythonファイルのEXE化
pyinstallerというものを使うとEXE化できる。こちら(pyinstallerの使い方【Python】)に詳しい説明あり。ここでは、下記のごとく実行し、コンソール出力なし、1ファイル化した。
> pyinstaller sshtest.py --noconsole --onefile
EOF