チャットプログラム
一対一のチャットプログラムである。適宜、ChatGPT利用して作成。
方針
IPアドレスを指定し、UDPにより、テキストを送信する。また、ポート番号も指定可能。下記「実行結果」を見れば、イメージは掴めると。
実行結果
入力後、リターン(エンターキー押下)により送信される。入力テキストは黒字、受信テキストは赤字で表示される。
簡略化のため、入力中に受信した場合は、即座に表示せず、右上に「メッセージあり」と表示し(左Window)、入力(送信)完了後、受信テキストを表示。
Ctrl-vでペーストされたものは、即送信される。(右Windowから左Window)
「ログ」をチェックすると、ログファイルを作成する。
ログファイルの内容は下記。
フォントサイズの選択も可能。
同じポートで複数起動すると、「Bind Errow」を表示。
ソースコード
コード中に記載済み事項もあるが、あえて追加(+同じ)の説明を下記する。
ライブラリ
## 利用モジュール(ライブラリ)
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/Stopボタン
テキスト受信スレッド
###### 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 ######
最後に
キー関連の例外処理が多い。テキストエディタの制御は大変であることを実感。