0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ろうとるがPythonを扱う、、(その35:チャット)

Posted at

チャットプログラム

一対一のチャットプログラムである。適宜、ChatGPT利用して作成。

方針

IPアドレスを指定し、UDPにより、テキストを送信する。また、ポート番号も指定可能。下記「実行結果」を見れば、イメージは掴めると。

実行結果

入力後、リターン(エンターキー押下)により送信される。入力テキストは黒字、受信テキストは赤字で表示される。

11.PNG

簡略化のため、入力中に受信した場合は、即座に表示せず、右上に「メッセージあり」と表示し(左Window)、入力(送信)完了後、受信テキストを表示。

12メッセージあり.PNG

Ctrl-vでペーストされたものは、即送信される。(右Windowから左Window)

13ペースト - コピー.PNG

「ログ」をチェックすると、ログファイルを作成する。

ログファイルの内容は下記。

フォントサイズの選択も可能。

同じポートで複数起動すると、「Bind Errow」を表示。

15エラー.PNG

ソースコード

コード中に記載済み事項もあるが、あえて追加(+同じ)の説明を下記する。

ライブラリ

## 利用モジュール(ライブラリ)
import sys
import time 
from datetime import datetime
from socket import socket, AF_INET, SOCK_DGRAM, inet_aton
import select
import threading 
import logging
import logging.handlers
from tkinter import * 
import tkinter.ttk as ttk
import tkinter.font as tkfont

ここは説明なし。

パラメータなど

## Version
version = 'v0.97'

## パラメーター
param = [' IPアドレス ', ' ポート ']	# パラメーター
p_width = [16, 5]			# エントリの幅
param_default = ['127.0.0.1', '60001']	# デフォルト値

## フラグ
# 実行状況(doing)
Stopped = 0
Started = 1
# キー入力状態(entered)
NoKeyInput = 0		# 未入力
KeyInputting = 1	# 入力中
KeyInputDone = 2	# 入力完了

## ボタン
btn_list = ['Start', 'Stop']	# doing: Stopped, Started

## フォントサイズ
default_font_size = 12
size_list = [default_font_size, default_font_size+6, default_font_size+12]
  • パラメーター(IPアドレス、ポート)およびデフォルト値
  • 実行状況フラグおよびキー入力状態変数
  • ボタン
  • フォントサイズ

キー入力処理

全般

###### Start of on_key() ######
## キー入力時処理
def on_key(event):
    global entered
    # 下記キーは通常動作(何もしない)
    if event.keysym in\
       ('Shift_L', 'Shift_R', 'Control_L', 'Control_R',\
        'Alt_L', 'Alt_R', 'Up', 'Down', 'Caps_Lock', 'BackSpace', 'Escape'):
        return
    # 入力中へ変更
    entered = KeyInputting
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置
    idx_end = frm2.txt.index('end')		# 末尾
    insert_line = idx_insert.split('.')[0]	# キャレットがある行
    end_line = idx_end.split('.')[0]		# 末尾(最終行+1)
    # マウスで上部をクリック時など、最終行にキャレットを移動
    if not (int(insert_line)+1 == int(end_line)):
        frm2.txt.mark_set('insert', 'end-1c')
        frm2.txt.see('end')
###### End of on_key() ######
  • 特殊キー(シフトなど)は何もしない
  • キー入力状態へ(変数entered)
  • 現在のキャレット(カーソル)行がテキスト最終行-1でない場合は、キャレットを最終行に移動
    • 常に最終行の次行で入力を行うよう制御
    • index('end')は常に最終行+1らしい

リターン(Enter)

###### Start of on_return() ######
## Return(Enter)入力時処理
def on_return(event):
    global entered, input_text
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置
    idx_end = frm2.txt.index('end')		# 末尾(最終行+1)
    insert_line = idx_insert.split('.')[0]	# 行番号Get
    end_line = idx_end.split('.')[0]		# 末尾(最終行+1)
    # マウスで上部をクリック後のReturn、最終行にキャレットを移動するのみ
    if not (int(insert_line)+1 == int(end_line)):
        frm2.txt.mark_set('insert', 'end-1c')
        frm2.txt.see('end')
        return
    # 行先頭から行末までの文字列をGet
    line_start = f'{insert_line}.0'
    line_end = f'{insert_line}.end'
    input_text = frm2.txt.get(line_start, line_end)
    # 行途中にキャレットがあっても、そこで改行せず、
    # 新しい行を末尾に追加して移動
    frm2.txt.insert('end', '\n')
    next_line = int(insert_line) + 1
    frm2.txt.mark_set('insert', f'{next_line}.0')
    frm2.txt.see('insert')
    # キー入力完了へ
    entered = KeyInputDone
    # 
    return 'break'	# 通常機能(画面上での改行挿入)をキャンセル
###### End of on_return() ######
  • 1行単位のチャットを意図した処理
  • 現在のキャレット(カーソル)行が末尾にない場合、キャレットを最終行に移動するのみ
  • 行先頭から行末までの文字列(入力文字)を取得(変数input_text:送信対象)
  • 行途中にキャレットがあっても改行せず、新しい行を末尾に追加して移動
  • キー入力完了状態へ(変数entered)
  • 「break」により、通常機能(画面上での改行挿入)をキャンセル

バックスペース

###### Start of on_backspace() ######
## バックスペース処理:行内のみ有効
def on_backspace(event):
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置(ex: '2.0')
    idx_end = frm2.txt.index('end')		# 末尾(最終行+1)
    insert_line, insert_column = map(int, idx_insert.split('.'))
    end_line, end_column = map(int, idx_end.split('.'))
    if (insert_column == 0) or not (int(insert_line)+1 == int(end_line)):
        # 行の先頭やキャレットが最終行にない場合
        return 'break'	# 通常機能(位置の戻り)キャンセル
###### End of on_backspace() ######
  • 現在のキャレット行および末尾の行および列位置を取得
  • 行の先頭やキャレットが最終行のみバックスペースを行う(1行単位のチャットのため)

Ctrl-V(ペースト)

###### Start of after_paste() ######
## 改行挿入
def after_paste():
    frm2.txt.insert(END, '\n')
    frm2.txt.see('end')
###### End of after_paste() ######
    
###### Start of on_paste() ######
## ペースト処理(クリップボード内容即送信)
def on_paste(event):
    global entered, input_text
    try:
        input_text = root.clipboard_get()
    except:
        return
    entered = KeyInputDone
    # ペースト最終行が2回送信されないよう改行挿入
    # イベント完了後の実行が必須
    root.after_idle(after_paste)
###### End of on_paste() ######
  • ペーストされた内容を即送信対象(変数input_text)とする
  • キー入力完了状態へ(変数entered)
  • ペースト後に改行

デフォルト操作無効化

###### Start of disable_keys() ######
## キーのデフォルト動作無効化
def disable_keys(event):
    return "break"
###### End of disable_keys() ######
  • 「return "break"」によるキーのデフォルト動作無効化
    • Upキーが対象(後述)

IPアドレスチェック

###### Start of is_valid_ip() ######
## IPアドレスの正当性チェック
def is_valid_ip(addr):
    try:
        inet_aton(addr)
        return True
    except:
        return False
###### End of is_valid_ip() ######
  • inet_aton()を利用したIPアドレス正当性チェック(定番)

Start/Stopボタン

###### Start of start_clk() ######
## Start/Stopクリック時処理
def start_clk():
    global doing, log, fh
    if doing == Stopped:	# Startクリック
        doing = Started
        frm2.txt.config(state='normal')
        # 入力パラメーター取得
        input = [frm1.text[i].get() for i in range(len(param))]
        # 未入力時、デフォルトパラメーターセット
        for i in range(len(param)):
            if input[i] == "":
                input[i] = param_default[i]
        # エラー時、デフォルトパラメーターセット
        try:
            if is_valid_ip(input[0]) == False:
                dest = param_default[0]
            else:
                dest = input[0]
            port = int(input[1])
        except:
            dest = param_default[0]
            port = param_default[1]
        # ログ作成(初期化)
        if logged:
            log = logging.getLogger('Chat')
            log.setLevel(logging.DEBUG)
            file_name = dest+'{:_%Y%m%d-%H%M%S}.log'.format(datetime.now())
            fh = logging.FileHandler(file_name)
            log.addHandler(fh)
        # Chatスレッド起動
        threading.Thread(target=chat, args=(dest, port), daemon=True).start()
    else:			# Stopクリック
        doing = Stopped
        # テキスト領域入力禁止
        frm2.txt.config(state='disabled')
        # ログ終了
        if 'log' in globals():
            fh.close()
            log.removeHandler(fh)
    # ボタン表記変更(Start <--> Stop)
    frm1.Btn['text'] = btn_list[doing]
###### End of start_clk() ######
  • Startクリック
    • 実行(Start)状態へ
    • テキストエリア入力有効化
    • 入力パラメーター取得(送信先IPアドレスおよびポート番号)
      • エラー時にはデフォルトパラメーターセット
    • ログ作成(初期化)
    • Chatスレッド起動
  • Stopクリック
    • Stop状態へ
    • テキストエリア入力無効化
    • ログ終了
  • ボタン表示変更(Start <--> Stop)

メニュー「ログ」

###### Start of log_tgl() ######
## メニュー’ログ’のトグル処理
def log_tgl():
    global logged
    tgl.set(not tgl.get())
    if tgl.get():
        logged = True
    else:
        logged = False
    return
###### End of log_tgl() ######
  • メニュー「ログ」のトグル処理(チェックマーク処理)
    • ログ有効・無効

メニュー「フォントサイズ」

###### Start of sel_font() ######
# フォントサイズ選択
def sel_font():
    global font_size
    font_size = f'{tgl_font.get()}'
    #print(font_size)
    frm2.txt['font'] = ('', font_size, 'bold')
    frm2.txt.tag_configure('bold_red', foreground='red', font=('', font_size, 'bold'))
###### End of sel_font() ######
  • メニュー「フォントサイズ」のトグル処理(チェックマーク処理)
    • 大、中、小(後述)

メイン

###############  Main  ###############
## Window
root = Tk()
ver_str = '簡易チャット ' + version
root.title(ver_str)
root.geometry("700x600")

## フラグ
# Start/Stop
doing = Stopped
# キー入力状態
entered = NoKeyInput

## 送信テキスト初期化
input_text = ''
  • Window作成
  • パラメーター初期化
## メニュー
menu_font = tkfont.Font(family='Meiryo', size=12)
# ファイル
logged = False
tgl = BooleanVar(value = logged)
menubar = Menu()
filemenu = Menu(menubar, tearoff=0, bg='lightgray', fg='black', font=menu_font)
menubar.add_cascade(label='ファイル', menu=filemenu)
filemenu.add_checkbutton(label='ログ', command=log_tgl)
filemenu.add_command(label='終了', command=sys.exit)
# フォントサイズ
font_size = default_font_size	# small
tgl_font = IntVar(value=size_list[0])
fontmenu = Menu(menubar, tearoff=0, bg='lightgray', fg='black', font=menu_font)
menubar.add_cascade(label='フォントサイズ', menu=fontmenu)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[0], command=sel_font)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[1], command=sel_font)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[2], command=sel_font)
  • メニュー作成
    • ファイル(ログ、終了)
    • フォントサイズ(大、中、小)
## Window内フレーム
# パラメータ入力エントリおよびStart/Stopボタン
frm1 = Frame(root, bd=1)
frm1.label = [Label(frm1, text=param[i], font=('', 12, 'bold')) for i in range(len(param))]
[frm1.label[i].grid(row=0, column=i*2) for i in range(len(param))]
frm1.text = [Entry(frm1, width=p_width[i], font=('', 12, 'bold')) for i in range(len(p_width))]
[frm1.text[i].grid(row=0, column=(i*2+1)) for i in range(len(p_width))]
frm1.dummy = Label(frm1, text='  ', font=('Courier New', 12))
frm1.dummy.grid(row=0, column=len(param)+len(p_width))
frm1.Btn = ttk.Button(frm1, text=btn_list[doing], style='W.TButton', command=lambda:start_clk())
frm1.Btn.grid(row=0, column=len(param)+len(p_width)+1)
[frm1.text[i].bind('<Return>', lambda event:start_clk()) for i in range(len(param))]
frm1.Btn.bind('<Return>', lambda event:start_clk())
frm1.msg = Label(frm1, text='        ', foreground='red', font=('', 12, 'bold'))
				# メッセージと同じ文字数分の空白
frm1.msg.grid(row=0, column=len(param)+len(p_width)+2)
# Chat用テキスト領域
frm2 = Frame(root, bd=1)
frm2.txt = Text(frm2, font=('', font_size, 'bold'), width=75, height=30, bg='PapayaWhip')
frm2.txt.tag_configure('bold_red', foreground='red', font=('', font_size, 'bold'))
frm2.txt.bind('<Key>', on_key)
frm2.txt.bind('<KeyPress-BackSpace>', on_backspace)
frm2.txt.bind("<Control-v>", on_paste)
frm2.txt.bind("<Control-V>", on_paste)
frm2.txt.bind('<Return>', on_return)
frm2.txt.bind('<Up>', disable_keys)
frm2.ysc = Scrollbar(frm2, orient=VERTICAL, command=frm2.txt.yview)
frm2.ysc.pack(side=RIGHT, fill='y')
frm2.txt['yscrollcommand'] = frm2.ysc.set
frm2.txt.config(state='disabled')	# テキスト領域入力禁止
frm2.txt.pack()
# フレーム配置
frm1.pack()
frm2.pack()

root.config(menu=menubar)

## ループ
root.mainloop()
  • フレーム作成
    • パラメータ入力エントリおよびStart/Stopボタン
      • ボタンクリック時の関数
    • Chat用テキスト領域
      • 各種キー入力時の関数

テキスト受信スレッド

###### Start of recv_text() ######
## テキスト受信スレッド
def recv_text(port):
    global doing
    sock = socket(AF_INET, SOCK_DGRAM)	# UDP
    try:
        sock.bind(('0.0.0.0', port))	# 全Interfaceで待ち受け
    except:
        frm2.txt.insert(END, 'Bind Error\n', 'bold_red')	# 赤太字で出力
        frm2.txt.see('end')
        doing = Stopped
        # ボタン表記変更
        frm1.Btn['text'] = btn_list[doing]
        return
    #
    fds = set()
    fds.add(sock)
    # ループ
    while True:
        out = ''
        try:			# 受信待ち
            r, w, x = select.select(list(fds), [], [], 0.1)
            for s in r:
                if s is sock:	# 受信
                    msg, cliaddr = s.recvfrom(1024)
                    out = msg.decode(encoding='utf-8')
                    out += '\n'
        except:
            out = 'Some Errors.' + '\n'
        if len(out):
            frm1.msg['text'] = ' メッセージあり'
            while entered == KeyInputting:	# キー入力中は表示待ち
                time.sleep(0.1)
            frm1.msg['text'] = '        '	# メッセージ消去		
            # 表示
            frm2.txt.insert(END, out, 'bold_red')	# 赤太字で出力
            frm2.txt.see('end')
            # ログ化
            if logged and 'log' in globals():
                dt = '{:%H%M%S}'.format(datetime.now())
                log.debug(dt+' Recv: '+out.rstrip('\n'))
        if doing == Stopped:	# Stopクリックで終了
            break
    # end of while
    sock.close()
###### End of recv_text() ######
  • UDPでbind()
  • select()による受信待ち
  • 入力中は即表示せず、メッセージ表示
  • 赤太字で表示
  • ログ
  • Stopクリックで終了
    • クリックされなければselect()で受信待ち

Chat

送信およびテキスト受信スレッドを起動。

###### Start of chat() ######
## Chatスレッド(送信および受信スレッド起動)
def chat(dest, port):
    global entered, input_text
    # 受信スレッド起動
    threading.Thread(target=recv_text, args=(port,), daemon=True).start()
    # テキスト(Chat)エリアのキャレットブリンク
    frm2.txt.focus_set()
    # ループ
    while True:
        if doing == Stopped:	# Stopクリックで終了
            break
        #
        if entered == KeyInputDone:	# キー入力完了
            # 送信
            sock = socket(AF_INET, SOCK_DGRAM)	# UDP
            sock.sendto(input_text.encode('utf-8'), (dest, port))
            # ログ化
            if logged and 'log' in globals():
                dt = '{:%H%M%S}'.format(datetime.now())
                log.debug(dt+' Send: '+input_text)
            entered = NoKeyInput	# 未入力状態へ
            input_text = ''		# 送信テキスト初期化
        #  
        time.sleep(0.1)
    # end of while
###### End of chat() ######
  • 受信スレッド起動
  • テキスト(表示)エリアのキャレットブリンク
  • Stopクリックで終了
  • キー入力完了時、UDPによる送信
    • キー未入力状態へ
  • ログ

全体

# -*- coding: utf-8 -*-
## 利用モジュール(ライブラリ)
import sys
import time 
from datetime import datetime
from socket import socket, AF_INET, SOCK_DGRAM, inet_aton
import select
import threading 
import logging
import logging.handlers
from tkinter import * 
import tkinter.ttk as ttk
import tkinter.font as tkfont

## Version
version = 'v0.97'

## パラメーター
param = [' IPアドレス ', ' ポート ']	# パラメーター
p_width = [16, 5]			# エントリの幅
param_default = ['127.0.0.1', '60001']	# デフォルト値

## フラグ
# 実行状況(doing)
Stopped = 0
Started = 1
# キー入力状態(entered)
NoKeyInput = 0		# 未入力
KeyInputting = 1	# 入力中
KeyInputDone = 2	# 入力完了

## ボタン
btn_list = ['Start', 'Stop']	# doing: Stopped, Started

## フォントサイズ
default_font_size = 12
size_list = [default_font_size, default_font_size+6, default_font_size+12]
## 

###### Start of recv_text() ######
## テキスト受信スレッド
def recv_text(port):
    global doing
    sock = socket(AF_INET, SOCK_DGRAM)	# UDP
    try:
        sock.bind(('0.0.0.0', port))	# 全Interfaceで待ち受け
    except:
        frm2.txt.insert(END, 'Bind Error\n', 'bold_red')	# 赤太字で出力
        frm2.txt.see('end')
        doing = Stopped
        # ボタン表記変更
        frm1.Btn['text'] = btn_list[doing]
        return
    #
    fds = set()
    fds.add(sock)
    # ループ
    while True:
        out = ''
        try:			# 受信待ち
            r, w, x = select.select(list(fds), [], [], 0.1)
            for s in r:
                if s is sock:	# 受信
                    msg, cliaddr = s.recvfrom(1024)
                    out = msg.decode(encoding='utf-8')
                    out += '\n'
        except:
            out = 'Some Errors.' + '\n'
        if len(out):
            frm1.msg['text'] = ' メッセージあり'
            while entered == KeyInputting:	# キー入力中は表示待ち
                time.sleep(0.1)
            frm1.msg['text'] = '        '	# メッセージ消去		
            # 表示
            frm2.txt.insert(END, out, 'bold_red')	# 赤太字で出力
            frm2.txt.see('end')
            # ログ化
            if logged and 'log' in globals():
                dt = '{:%H%M%S}'.format(datetime.now())
                log.debug(dt+' Recv: '+out.rstrip('\n'))
        if doing == Stopped:	# Stopクリックで終了
            break
    # end of while
    sock.close()
###### End of recv_text() ######

###### Start of chat() ######
## Chatスレッド(送信および受信スレッド起動)
def chat(dest, port):
    global entered, input_text
    # 受信スレッド起動
    threading.Thread(target=recv_text, args=(port,), daemon=True).start()
    # テキスト(Chat)エリアのキャレットブリンク
    frm2.txt.focus_set()
    # ループ
    while True:
        if doing == Stopped:	# Stopクリックで終了
            break
        #
        if entered == KeyInputDone:	# キー入力完了
            # 送信
            sock = socket(AF_INET, SOCK_DGRAM)	# UDP
            sock.sendto(input_text.encode('utf-8'), (dest, port))
            # ログ化
            if logged and 'log' in globals():
                dt = '{:%H%M%S}'.format(datetime.now())
                log.debug(dt+' Send: '+input_text)
            entered = NoKeyInput	# 未入力状態へ
            input_text = ''		# 送信テキスト初期化
        #  
        time.sleep(0.1)
    # end of while
###### End of chat() ######

###### Start of on_backspace() ######
## バックスペース処理:行内のみ有効
def on_backspace(event):
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置(ex: '2.0')
    idx_end = frm2.txt.index('end')		# 末尾(最終行+1)
    insert_line, insert_column = map(int, idx_insert.split('.'))
    end_line, end_column = map(int, idx_end.split('.'))
    if (insert_column == 0) or not (int(insert_line)+1 == int(end_line)):
        # 行の先頭やキャレットが最終行にない場合
        return 'break'	# 通常機能(位置の戻り)キャンセル
###### End of on_backspace() ######

###### Start of after_paste() ######
## 改行挿入
def after_paste():
    frm2.txt.insert(END, '\n')
    frm2.txt.see('end')
###### End of after_paste() ######
    
###### Start of on_paste() ######
## ペースト処理(クリップボード内容即送信)
def on_paste(event):
    global entered, input_text
    try:
        input_text = root.clipboard_get()
    except:
        return
    entered = KeyInputDone
    # ペースト最終行が2回送信されないよう改行挿入
    # イベント完了後の実行が必須
    root.after_idle(after_paste)
###### End of on_paste() ######

###### Start of on_key() ######
## キー入力時処理
def on_key(event):
    global entered
    # 下記キーは通常動作(何もしない)
    if event.keysym in\
       ('Shift_L', 'Shift_R', 'Control_L', 'Control_R',\
        'Alt_L', 'Alt_R', 'Up', 'Down', 'Caps_Lock', 'BackSpace', 'Escape'):
        return
    # 入力中へ変更
    entered = KeyInputting
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置
    idx_end = frm2.txt.index('end')		# 末尾
    insert_line = idx_insert.split('.')[0]	# キャレットがある行
    end_line = idx_end.split('.')[0]		# 末尾(最終行+1)
    # マウスで上部をクリック時など、最終行にキャレットを移動
    if not (int(insert_line)+1 == int(end_line)):
        frm2.txt.mark_set('insert', 'end-1c')
        frm2.txt.see('end')
###### End of on_key() ######

###### Start of on_return() ######
## Return(Enter)入力時処理
def on_return(event):
    global entered, input_text
    # 位置情報取得
    idx_insert = frm2.txt.index('insert')	# 現在のキャレット位置
    idx_end = frm2.txt.index('end')		# 末尾(最終行+1)
    insert_line = idx_insert.split('.')[0]	# 行番号Get
    end_line = idx_end.split('.')[0]		# 末尾(最終行+1)
    # マウスで上部をクリック後のReturn、最終行にキャレットを移動するのみ
    if not (int(insert_line)+1 == int(end_line)):
        frm2.txt.mark_set('insert', 'end-1c')
        frm2.txt.see('end')
        return
    # 行先頭から行末までの文字列をGet
    line_start = f'{insert_line}.0'
    line_end = f'{insert_line}.end'
    input_text = frm2.txt.get(line_start, line_end)
    # 行途中にキャレットがあっても、そこで改行せず、
    # 新しい行を末尾に追加して移動
    frm2.txt.insert('end', '\n')
    next_line = int(insert_line) + 1
    frm2.txt.mark_set('insert', f'{next_line}.0')
    frm2.txt.see('insert')
    # キー入力完了へ
    entered = KeyInputDone
    # 
    return 'break'	# 通常機能(画面上での改行挿入)をキャンセル
###### End of on_return() ######

###### Start of is_valid_ip() ######
## IPアドレスの正当性チェック
def is_valid_ip(addr):
    try:
        inet_aton(addr)
        return True
    except:
        return False
###### End of is_valid_ip() ######

###### Start of start_clk() ######
## Start/Stopクリック時処理
def start_clk():
    global doing, log, fh
    if doing == Stopped:	# Startクリック
        doing = Started
        frm2.txt.config(state='normal')
        # 入力パラメーター取得
        input = [frm1.text[i].get() for i in range(len(param))]
        # 未入力時、デフォルトパラメーターセット
        for i in range(len(param)):
            if input[i] == "":
                input[i] = param_default[i]
        # エラー時、デフォルトパラメーターセット
        try:
            if is_valid_ip(input[0]) == False:
                dest = param_default[0]
            else:
                dest = input[0]
            port = int(input[1])
        except:
            dest = param_default[0]
            port = param_default[1]
        # ログ作成(初期化)
        if logged:
            log = logging.getLogger('Chat')
            log.setLevel(logging.DEBUG)
            file_name = dest+'{:_%Y%m%d-%H%M%S}.log'.format(datetime.now())
            fh = logging.FileHandler(file_name)
            log.addHandler(fh)
        # Chatスレッド起動
        threading.Thread(target=chat, args=(dest, port), daemon=True).start()
    else:			# Stopクリック
        doing = Stopped
        # テキスト領域入力禁止
        frm2.txt.config(state='disabled')
        # ログ終了
        if 'log' in globals():
            fh.close()
            log.removeHandler(fh)
    # ボタン表記変更(Start <--> Stop)
    frm1.Btn['text'] = btn_list[doing]
###### End of start_clk() ######

###### Start of disable_keys() ######
## キーのデフォルト動作無効化
def disable_keys(event):
    return "break"
###### End of disable_keys() ######

###### Start of sel_font() ######
# フォントサイズ選択
def sel_font():
    global font_size
    font_size = f'{tgl_font.get()}'
    #print(font_size)
    frm2.txt['font'] = ('', font_size, 'bold')
    frm2.txt.tag_configure('bold_red', foreground='red', font=('', font_size, 'bold'))
###### End of sel_font() ######

###### Start of log_tgl() ######
## メニュー’ログ’のトグル処理
def log_tgl():
    global logged
    tgl.set(not tgl.get())
    if tgl.get():
        logged = True
    else:
        logged = False
    return
###### End of log_tgl() ######

###############  Main  ###############
## Window
root = Tk()
ver_str = '簡易チャット ' + version
root.title(ver_str)
root.geometry("700x600")

## フラグ
# Start/Stop
doing = Stopped
# キー入力状態
entered = NoKeyInput

## 送信テキスト初期化
input_text = ''

## メニュー
menu_font = tkfont.Font(family='Meiryo', size=12)
# ファイル
logged = False
tgl = BooleanVar(value = logged)
menubar = Menu()
filemenu = Menu(menubar, tearoff=0, bg='lightgray', fg='black', font=menu_font)
menubar.add_cascade(label='ファイル', menu=filemenu)
filemenu.add_checkbutton(label='ログ', command=log_tgl)
filemenu.add_command(label='終了', command=sys.exit)
# フォントサイズ
font_size = default_font_size	# small
tgl_font = IntVar(value=size_list[0])
fontmenu = Menu(menubar, tearoff=0, bg='lightgray', fg='black', font=menu_font)
menubar.add_cascade(label='フォントサイズ', menu=fontmenu)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[0], command=sel_font)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[1], command=sel_font)
fontmenu.add_radiobutton(label='', variable=tgl_font, value=size_list[2], command=sel_font)

## Window内フレーム
# パラメータ入力エントリおよびStart/Stopボタン
frm1 = Frame(root, bd=1)
frm1.label = [Label(frm1, text=param[i], font=('', 12, 'bold')) for i in range(len(param))]
[frm1.label[i].grid(row=0, column=i*2) for i in range(len(param))]
frm1.text = [Entry(frm1, width=p_width[i], font=('', 12, 'bold')) for i in range(len(p_width))]
[frm1.text[i].grid(row=0, column=(i*2+1)) for i in range(len(p_width))]
frm1.dummy = Label(frm1, text='  ', font=('Courier New', 12))
frm1.dummy.grid(row=0, column=len(param)+len(p_width))
frm1.Btn = ttk.Button(frm1, text=btn_list[doing], style='W.TButton', command=lambda:start_clk())
frm1.Btn.grid(row=0, column=len(param)+len(p_width)+1)
[frm1.text[i].bind('<Return>', lambda event:start_clk()) for i in range(len(param))]
frm1.Btn.bind('<Return>', lambda event:start_clk())
frm1.msg = Label(frm1, text='        ', foreground='red', font=('', 12, 'bold'))
				# メッセージと同じ文字数分の空白
frm1.msg.grid(row=0, column=len(param)+len(p_width)+2)
# Chat用テキスト領域
frm2 = Frame(root, bd=1)
frm2.txt = Text(frm2, font=('', font_size, 'bold'), width=75, height=30, bg='PapayaWhip')
frm2.txt.tag_configure('bold_red', foreground='red', font=('', font_size, 'bold'))
frm2.txt.bind('<Key>', on_key)
frm2.txt.bind('<KeyPress-BackSpace>', on_backspace)
frm2.txt.bind("<Control-v>", on_paste)
frm2.txt.bind("<Control-V>", on_paste)
frm2.txt.bind('<Return>', on_return)
frm2.txt.bind('<Up>', disable_keys)
frm2.ysc = Scrollbar(frm2, orient=VERTICAL, command=frm2.txt.yview)
frm2.ysc.pack(side=RIGHT, fill='y')
frm2.txt['yscrollcommand'] = frm2.ysc.set
frm2.txt.config(state='disabled')	# テキスト領域入力禁止
frm2.txt.pack()
# フレーム配置
frm1.pack()
frm2.pack()

root.config(menu=menubar)

## ループ
root.mainloop()

###### End of Program ######

最後に

キー関連の例外処理が多い。テキストエディタの制御は大変であることを実感。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?