はじめに
若者のPC離れと言われる中、タイピングができない人が多くなってきているそうです。しかし若者はPCが使えない代わりにスマホの扱いには慣れており、現在の女子高生のフリック入力はガラケーギャルもびっくりするほどの速さです。
それならPCの入力もフリックでやってしまえば良いのではないか、ということは以前から言われていたようですが、手軽に試せるソフトはありませんでした。
そこで、iOSでPythonプログラミングができるPytoというアプリを使って、フリックキーボードを作ってみました。
既存の製品
-
FlickTyper
スマホとPCをこの装置で繋ぐとスマホがフリックキーボードになります。また、Bluetoothを使って無線で接続できるFlickTyperBTもあります。PC側に専用ドライバのインストールが不要である点が手軽で良さそうです。 -
Google日本語入力 物理フリックバージョン
2016年にGoogleが出した、エイプリルフールのネタです。 -
物理フリックキーボード
上のGoogleのネタが実用的になったようなキーボード組み立てキットです。個人で製作されたものだそうです。Switch Scienceで受託販売されていました。
他にも、タッチパネルを使ってフリックキーボードを作成しようとされている画像を見つけました。
環境
[iOS側]
iPhone SE 2020 (iOS13.4.1)
Pyto (12.0.3)
[PC側]
Windows 10 Home 64bit (1909)
Python 3.7.4
ライブラリ
pyautogui (0.9.50)
pyperclip (1.8.0)
仕様
ソケット通信を使ってiOS端末の入力データをPCに送ります。
機能は文字入力、Enter、Backspaceです。十字キーやDelete等の機能をつけることも可能ですが、今回は文字入力に限定しました。
漢字の変換はiOS側で行い、変換確定後、再度確定を押すことでPCに文字を送ります。
ソースコードと解説
ソースコード全体は GitHub に置いています。
iOS側が2ファイル、PC側が1ファイルです。
iOS側
text_client.py
# text_client.py
import socket
class Sender():
def __init__(self, port, ipaddr, timeout=5.0):
self.port = port
self.ipaddr = ipaddr
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
def connect(self):
self.sock.connect((self.ipaddr, self.port))
def send(self, text, enc="utf-8"):
"""
データを送信する
Parameters
----------
text : str
送信するテキスト
enc : str
エンコードのタイプ。Noneでエンコードなし
"""
if(enc is not None):
text_ = text.encode(enc)
else:
text_ = text
self.sock.sendall(text_)
def close(self):
self.sock.close()
テキスト送信クラスです。ソケット通信を使ってデータを送ります。これを2つ目のファイルで使います。
flick_kb.py
# flick_kb.py
import sys
import socket
import pyto_ui as ui
import text_client as myclient
class MySender(myclient.Sender):
def __init__(self, port, ipaddr, timeout=5.0):
print(port, ipaddr)
super().__init__(port, ipaddr, timeout)
self.editflag = False
def connect(self):
try:
super().connect()
self.editflag = True
print("connection")
except socket.timeout:
print("timed out")
super().close()
sys.exit()
def send_text(self, text):
self.send(text)
def send_end_flag(self):
# アプリ終了をPCに伝えるフラグを送信
if(self.editflag):
self.send(b"\x00", enc=None)
self.editflag = False
def close(self):
self.send_end_flag()
super().close()
def did_end_editing(self, sender):
if(self.editflag):
if sender.text == " ":
self.send(b"\x0a", enc=None) # enter
sender.text = " "
elif sender.text == "":
self.send(b"\x08", enc=None) # backspace
else:
self.send_text(sender.text[1:]) # 前のスペースを省いてテキスト送信
sender.text = " "
sender.superview["text_field1"].become_first_responder() # テキストボックスからフォーカスが外れないようにする
def did_change_text(self, sender):
if sender.text == "":
sender.text = " " # バックスペース検知用
self.send(b"\x08", enc=None) # backspace
def main():
args = sys.argv
if(len(args)<=2):
print("Input arguments. [PORT] [IP Address]")
sys.exit()
else:
port = int(args[1])
ipaddr = args[2]
# 送信部
print("start connection...")
mysender = MySender(port=port, ipaddr=ipaddr, timeout=5.0)
mysender.connect()
# GUI部
view = ui.View()
view.background_color = ui.COLOR_SYSTEM_BACKGROUND
text_field = ui.TextField(placeholder="Back Space")
text_field.name = "text_field1"
text_field.text = " "
text_field.become_first_responder()
text_field.action = mysender.did_change_text
text_field.did_end_editing = mysender.did_end_editing
text_field.return_key_type = ui.RETURN_KEY_TYPE_DONE
text_field.width = 300
text_field.center = (view.width/2, view.height/2)
text_field.flex = [
ui.FLEXIBLE_BOTTOM_MARGIN,
ui.FLEXIBLE_TOP_MARGIN,
ui.FLEXIBLE_LEFT_MARGIN,
ui.FLEXIBLE_RIGHT_MARGIN
]
view.add_subview(text_field)
ui.show_view(view, ui.PRESENTATION_MODE_SHEET)
# 終了処理
mysender.close()
print("end")
if __name__=="__main__":
main()
フリックキーボード本体です。pytoのpyto_ui.TextField
を使っています。GUI部はPytoのサンプルコードとほぼ同じです。
このTextField
ですが、確定を押すと自動的にフォーカスが外れてしまいます。それでは連続入力ができないため、become_first_responder()
という関数を呼び出し、再度表示させるという方法をとっています。このため、確定キー押下後に一瞬だけキーボードが消えます。
また、GUI部のtext_field.action
に設定しているmysender.did_change_text
メソッドは日本語の文字確定前でも呼び出されるので、変換を確定した瞬間に送信する、といったことができないと思われます。ゆえに「変換を確定→確定」と2回確定ボタンを押すことでテキストを送信するようにしました。改行を入れるには3回確定キーを押す必要があります。
文字消去(Backspace)は何もテキストがない状態では反応しません。そこで、あらかじめテキストボックスにスペースを入れておくようにしました。このスペースが消された時にBackspaceが押されたと判定します。消された後はsender.text = " "
という処理で、すぐに新しいスペースを入れています。
Enter、Backspace、終了コードはそれぞれUTF-8のLF (0x0a)、Backspace (0x08)、NULL(0x00)を使用しましたが、フラグとして使っているだけなので本来の意味はありません。そのため、改行コードはPCのOSやエディタの設定が反映されます。
PC側
flick_kb_receiver.py
# flick_kb_receiver.py
import sys
import time
import socket
import pyautogui
import pyperclip
import threading
def type_text(text):
# 与えた文字を入力(クリップボードにコピー&ペースト)
pyperclip.copy(text)
pyautogui.hotkey("ctrl", "v")
return True
def type_backspace():
pyautogui.typewrite(["backspace"])
return True
def type_enter():
pyautogui.typewrite(["enter"])
return True
class Receiver():
def __init__(self, port, ipaddr=None, set_daemon=True):
"""
受信側
Parameters
----------
port : int
使用するポート番号
ipaddr : None or str
受信側PCのIPアドレス.Noneで自動取得.
set_daemon : bool
スレッドをデーモン化するか.受信部スレッド終了を待たずにメインスレッドを停止させる.
"""
if(ipaddr is None):
host = socket.gethostname()
ipaddr = socket.gethostbyname(host)
self.ipaddr = ipaddr
self.port = port
self.set_daemon = set_daemon
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.loopflag = False
print("ip:{0} port:{1}".format(self.ipaddr, self.port))
def loop(self):
self.sock.settimeout(0.5)
self.sock.bind((self.ipaddr, self.port))
self.sock.listen(1)
print("start listening...")
while(self.loopflag):
try:
conn, addr = self.sock.accept()
except socket.timeout:
continue
print("accepted")
with conn:
while(self.loopflag):
# print("waiting...")
data = conn.recv(1024)
if(not data):
break
if(data[:1]==b"\x08"): # 連続してbackspaceを押すと,複数個同時に送られてくる(例:b"\x08\x08\x08)ことがあるため,先頭8バイトのみチェック
print("> BS")
type_backspace()
elif(data==b"\x0a"):
print("> Enter")
type_enter()
elif(data==b"\x00"):
print("STOP CLIENT")
break
else:
text = data.decode("utf-8")
print(">", text)
type_text(text)
def start_loop(self):
self.loopflag = True
self.thread = threading.Thread(target=self.loop)
if(self.set_daemon):
self.thread.setDaemon(True)
self.thread.start()
print("start_thread")
def stop_loop(self):
print("stop loop")
self.loopflag = False
time.sleep(0.6) # socketがtimeoutするまで待つ
if(not self.set_daemon):
print("waiting to stop client...") # 送信側が停止するのを待つ
self.thread.join()
print("stop_thread")
def close_sock(self):
self.sock.close()
print("socket closed")
def main():
# コマンドライン引数
ipaddr = None
args = sys.argv
if(len(args)<=1):
print("Usage: flick_kb_receiver [PORT] [IP (optional)]")
sys.exit()
elif(len(args)==2):
port = int(args[1])
else:
port = int(args[1])
ipaddr = args[2]
# メイン処理
receiver = Receiver(port=port, ipaddr=ipaddr)
receiver.start_loop()
while True:
stopper = input()
if(stopper=="s"):
receiver.stop_loop()
break
receiver.close_sock()
if __name__=="__main__":
main()
PC側です。文字入力にはpyautoguiを使いました。pyautoguiはキー操作やマウス操作等PC操作全般が可能なライブラリです。受信したバイナリデータはEnter、Backspace、終了コードでなければデコードしてテキスト入力処理へ回します。
しかし、pyautoguiでは日本語入力ができないそうなので、type_text
メソッドでテキストをクリップボードに保存してから貼り付ける、といった方法をとっています(参考)。
Backspaceについてですが、スマホ側にて連続で文字削除するとBackspaceコード\x08
が\x08\x08\x08
のように複数個同時に送られてくることがありました。そのため、if(data[:1]==b"\x08"):
という書き方で、先頭の1バイトだけを見るようにしています。ちなみにdata[0]
だと8
(10進数に変換された値)が返ってきます。
通信部は別スレッドで動かし、メインスレッドはプログラム終了コマンドを待つようにしています。実行中、コンソールにs
を入力しEnterを押すとプログラムを終了できます。しかし、通信がつながっている状態では通信部のスレッドが停止しないため、通信部をデーモン化しメインスレッド終了時に強制的に停止するようにしています。これが良い方法かは分かりません。
動作
ファイル配置
[PC]
flick_kb_receiver.py
[iOS]
flick_keyboard(任意のフォルダ)
├── flick_kb.py (実行ファイル)
├── text_client.py
実行手順
接続・入力
- PC側でポート番号をコマンドライン引数として実行
例:python flick_kb_receiver.py 50001
- iOS側のPytoアプリで
flick_kb.py
を開き、右下の歯車マークを押す。その中のArgumentsにポート番号とIPアドレスを入力後、実行
例:50001 192.168.1.2
- PCで任意のテキストエディタや検索窓等にカーソルを合わせる
- iOS側のテキストボックスに入力確定後、再度確定を押す
- PC側にテキストが入力される
切断・終了
- iOS側のプログラムを停止
- PC側のコンソールに"s"を入力後Enterを押し、PC側のプログラムを停止
iOS側でimport部のエラーが出た場合は、フォルダアクセスが許可されていないと思うので、Pytoエディタ画面右下の南京錠マークから許可してください。
PC側の初回起動時にファイアウォールのアクセス許可が出ますが、パブリックアクセスを許可しないとつながりませんでした。
動作の様子
上のGIFはPCとiPhoneの画面を後から合わせたものです。接続からVSCodeへの文字入力、切断まで行っています。遅延はほとんど感じられませんでした。
おわりに
小さいタッチパネルで日本語を入力するためのキーボードとして、フリック入力は非常に良い手法だと思います。今回、実際にPCでフリック入力を試してみて、PCキーボードが使えない人にフリック入力という選択肢が増えることでPCへの敷居が下がるのではないかと感じました。PC-スマホ間の視線移動も少なく、意外と違和感なく入力できました。
しかし、私はPCキーボードを使ったほうが速く打てるため、フリック入力を使うメリットはほとんどありません。片手で打てること、寝ながら使えることくらいでしょうか。やはりPCのキーボード入力は指すべてを生かすことができ、記号も打ちやすいので、使えるに越したことはないです。