LoginSignup
1
2

More than 1 year has passed since last update.

ろうとるがPythonでWindowsプログラムをつくる

Last updated at Posted at 2022-05-06

久しぶりにWindowsプログラム

事情があって、ウン十年ぶりにWindowsプログラムをつくることになった。できるだけトレンディを身につけたく調べていたところ、Python+tkinterでWindowsプログラムを実現できることがわかったので、その記録をここに残すこととした。小生、Pythonは初心者レベル、tkinterは初めてのトライであり、基本的なことも記載。

tkinter

Python上でGUIを実現するもの(ライブラリ)。山のように情報が見つかる。

やること

sshでLinuxにログインし、"ls"にてホームディレクトリのファイルやディレクトリの一覧を取得し、"ls -ld"にて選択したファイルまたはディレクトリの詳細を表示するもの。
全容.png
内容にあまり意味はなく、作成しながら学びたいこと(下記内容)を積み上げたら、このようなプログラムとなった。

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”を返す
    下記はエラー表示の例である。
    ConnectionError.png
### 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」を参考にした。
Combobox.png

### 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にて表示する。なお、コンボボックスにて選択されていないときは、エラー表示する。
No selected File.png

### 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”クリック時にはプログラムが終了。
メニュー.png
Debug.png

# 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時のログファイルのサンプルである。

Log-20220320-081850.log
==== 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

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2