Python tkinter GUIプログラミング テーブル情報取得

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、データベースオラクルへ接続、そのスキーマのテーブル情報を取得してTreeviewへ表示するプログラムにチャレンジしました。いや~、けっこう時間がかかりましたねぇ。Treeviewを思ったように操作するのが難しかったなぁ。

 Oracleのバージョンは18c Express Edition、PythonはAnaconda3です。

出来上がりイメージ

 今回のソースを実行して、「load table」ボタンをクリックすると、こんな感じのウィンドウになります。左側のテーブル一覧を選択すると右側に項目の一覧が表示されます。

できあがりイメージ

ソースコード

import cx_Oracle as db
import tkinter as tk
import tkinter.ttk as ttk

LOGIN_USER = "ユーザー名をここに入力"
LOGIN_PASSWORD = "パスワードをここに入力"
LOGIN_HOST = "localhost:1521/xepdb1"

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Table viewer sample for ORACLE XE 18c")

        self.conn = db.connect(LOGIN_USER, LOGIN_PASSWORD, LOGIN_HOST)
        self.cur = self.conn.cursor()
        
        self.style = ttk.Style(self)
        self.style.configure("Treeview", rowheight=30)
        
        columns = ("#1", "#2")
        self.table_list = ttk.Treeview(self, show="headings", columns=columns, selectmode="browse")
        self.table_list.heading("#1", text="TABLE_NAME")
        self.table_list.column("#1", width=125)
        self.table_list.heading("#2", text="COMMENTS")
        self.table_list.column("#2", width=300)
        self.table_list.grid(row=0, column=0, rowspan=2, sticky=tk.NSEW)
        self.scroll_bar = ttk.Scrollbar(self, command=self.table_list.yview)
        self.table_list.configure(yscroll=self.scroll_bar.set)
        self.scroll_bar.grid(row=0, column=1, rowspan=2, sticky=tk.NS)
        self.table_list.bind("<<TreeviewSelect>>", self.show_table)
        
        columns = ("#1","#2","#3")
        self.table_info = ttk.Treeview(self, show="headings", columns=columns)
        self.table_info.heading("#1", text="COLUMN_NAME")
        self.table_info.heading("#2", text="COMMENTS")
        self.table_info.heading("#3", text="DATA_TYPE")
        self.table_info.grid(row=0, column=2, sticky=tk.NSEW)
        
        self.load_button = ttk.Button(self, text="load table", command=self.load_table)
        self.load_button.grid(row=1, column=2)
        
        self.grid_rowconfigure(0, weight=1)

    def load_table(self):
        self.table_list.delete(*self.table_list.get_children())
        self.table_info.delete(*self.table_info.get_children())
        sql = ("SELECT TABLE_NAME, COMMENTS"
               "  FROM USER_TAB_COMMENTS"
               " WHERE TABLE_TYPE = 'TABLE'")
        results = self.cur.execute(sql)
        for row in results:
            self.table_list.insert("", tk.END, values=row)
        self.table_list.selection_set(self.table_list.get_children()[0])
    
    def show_table(self, event):
        sel = self.table_list.selection()
        table_name = self.table_list.item(sel[0])["values"][0]
        sql = ("SELECT T1.COLUMN_NAME, T1.COMMENTS,"
               "  CASE WHEN T2.DATA_TYPE IN ('TIMESTAMP(6)','DATE') THEN T2.DATA_TYPE"
               "       WHEN T2.DATA_TYPE IN ('CHAR','VARCHAR2') THEN T2.DATA_TYPE ||'('||T2.DATA_LENGTH||')'"
               "       WHEN T2.DATA_TYPE = 'NUMBER' THEN T2.DATA_TYPE || "
               "         CASE WHEN T2.DATA_PRECISION IS NULL AND T2.DATA_SCALE IS NULL THEN ''"
               "              WHEN T2.DATA_PRECISION IS NOT NULL AND NVL(T2.DATA_SCALE,0) = 0 THEN '('||T2.DATA_PRECISION||')'"
               "              WHEN T2.DATA_PRECISION IS NOT NULL AND T2.DATA_SCALE IS NOT NULL THEN '('||T2.DATA_PRECISION||'.'||T2.DATA_SCALE||')'"
               "              WHEN T2.DATA_PRECISION IS NULL THEN '(.'||T2.DATA_SCALE||')'"
               "              END"
               "       END"
               "  FROM USER_COL_COMMENTS T1, USER_TAB_COLUMNS T2"
               " WHERE T1.TABLE_NAME = T2.TABLE_NAME"
               "   AND T1.COLUMN_NAME = T2.COLUMN_NAME"
               "   AND T1.TABLE_NAME = :table_name")
        results = self.cur.execute(sql, table_name=table_name)
        self.table_info.delete(*self.table_info.get_children())
        for row in results:
            self.table_info.insert("", tk.END, values=row)
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

解説

 Oracleへの接続にはおなじみのcx_Oracleを利用しました。cx_Oracleのセットアップについては、こちらを参考にしてください。

 5~7行目の接続情報はご自分の環境にあわせて変更してください。Oracle18c Express Editionをデフォルトインストールしたぼくの環境では、このLOGIN_HOSTで接続できました。もしかしたらlocalhostをローカルPCのIPアドレスに変更する必要があるかも知れません。

 14、15行目の初期化処理で、Oracleへの接続とカーソル取得しています。21行目でTreeviewを作成しています。showオプションは”tree”と”heading”が指定できますが、組み合わせで以下のように変わってきます。

  • “tree” カラム#0を表示します。
  • “headings” ヘッダ行を表示します。
  • “tree headings” カラム#0とヘッダ行の両方を表示します。(デフォルト値)
  • “” カラム#0もヘッダ行も表示しない。

 selectmodeは、”extended”、”browse”、”none”が選択可能で、以下のような意味があります。

  • “extended” 複数アイテムが選択可能。(デフォルト値)
  • “browse” 単一のアイテムが選択可能。
  • “none” 選択状態は変わらない。

 ちなみに、このTreeviewの行間、ぼくの環境Windows10では低く設定されていて文字が切れてしまっていたので、17、18行目のtkk.Styleを使って”Treeview”のrowheightを30にセットしています。

 スクロールバーは27、28行目で作成してセットしています。イディオムだと思って覚えてしまいましょう。30行目でバインドしているのは、仮想イベントの<<TreeviewSelect>>です。セレクションが変更されたときに発生します。ここでは、show_tableをバインドしています。これで選択されたテーブルの情報を隣のTreeviewに表示しています。

 42行目のself.grid_rowconfigure(0, weight=1)ですが、ウィンドウのサイズを変更したときに、行がどれくらいの割合で広がるか、というのを設定しています。指定しない場合のweightは0にセットされています。これによりgridで指定された0行目がウィンドウのサイズ変更に伴って変更されます。ちなみに、ウィジェットにgridを適用するときにstickyを指定しない場合は大きさは変わらないので注意が必要です。

 45、46行目では、Treeviewの項目を削除しています。すべての項目を取得するのに、self.table_list.get_children()を利用しています。このメソッドはすべての項目をタプルで戻します。self.table_list.delete()の引数は、削除したい項目をすべて渡す必要があるため、先頭に「*」アスタリスクを付けて、タプルのアンパックをしています。タプルのままでは動作しませんので、注意が必要です。

 47~49行目はSQL文を定義しています。複数文字列をインデントを無視した形で改行するのに丸括弧「()」を使っています。丸括弧の中はインデント無視できるのですよね。ちなみにここで指定しているテーブルのUSER_TAB_COMMENTSは、オラクルのデータディクショナリで、テーブルに設定されているコメントを参照することができるビューになります。

 SQL文の実行は50行目です。51行目で結果セットを順番に取り出すforループを記述しています。データを取得して、ループして一行ずつ処理する、このパターンもよく使いますので、覚えておくと便利です。

 Treeviewへのデータの追加はinsert()メソッドです。valuesパラメータを使うことで複数項目を一気にセットすることができます。

 あと、53行目は、テーブルを取得したときに、1行目を選択させて、テーブル情報を右側に表示するために追加しました。1行目を選択するのに結構悩みましたよ~。selection_set()メソッドへ渡すパラメータは項目名なのですよね。0とか-1とかを指定してもエラーになります。そこで、ここでは、self.table_list.get_children()[0]を使って最初の項目を取得しています。この0を-1に変更すると最後の項目、任意の行の場合も数値を変更するだけで対応可能ですね。

 56行目、sel = self.table_list.selection()は、選択中のアイテム名を取得しています。取得結果は複数の場合もあるのでタプルで戻ってきます。ここでは一項目しか取得できない設定ですので、sel[0]と指定すれば、選択中の項目が利用可能になります。57行目のtable_name = self.table_list.item(sel[0])["values"][0]で、Treeviewにセットされた値を取得することができます。

まとめ

 いや~、時間かかったなぁ。あ、二つ目のSQLの説明を忘れてました。ま、そんなに難しくないし、見ればわかるか。Treeviewの扱いに慣れるのには、ちょっと時間がかかりそうですね。またサンプル作ってみますね。

EXCEL 絶対パスからファイル名のみ取り出す最速のやり方

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、お仕事で調査をしているときに、EXCELで絶対パスからファイル名だけを切り取るのをどうやったらいいのかを調べるのに、ちょっと時間がかかってしまったので、まとめておきます。

やりたいこと

 以下のように、エクセルに書かれた絶対パスからファイル名を取り出す。

絶対パスからファイル名のみを取り出す

 Pythonならすぐにこんな感じでできちゃうのですけどねぇ。

In [1]: from os.path import basename
   ...: basename(r"C:\work\tmframework\modebi-fw\TMD\MODEBI_FW_サンプルデータ.xlsx")
Out[1]: 'MODEBI_FW_サンプルデータ.xlsx'

 ま、エクセルですばやくどうするのか、とういことですから、きっとエクセルの関数を使うのがよいでしょうねぇ。で、調べて見つかったのが二つありました。

短めのやりかた

 最初に見つけたのは、このやり方です。

=TRIM(RIGHT(SUBSTITUTE(B2,"\",REPT(" ",100)),100))

 中身を説明すると、SUBSTITUTE関数で、区切り文字の「\」を空白100文字に置き換えて、右から100文字切り取って、そのあと空白を消す、という感じです。SUBSITUTE関数は、第一引数の文字列から、第二引数の文字列を探し出して、第三引数に置き換える関数です。REPT()関数は第一引数(この場合は半角スペース” “)を第二引数の個数分繰り返す関数で、100文字分の半角スペースを作っています。100文字を超えるファイル名の場合は使えないやり方ですが、Windowsではきっと問題ありませんね。ちょっとトリッキーなやり方だと思いますが、割と短く書くことができる、良いやり方だと思いました。ただ、「100」はあんまり好きじゃないのですよねぇ。

正統派(?)なやり方

 そして、もうちょっと探して見つけたのが次のやり方です。

=MID(B3,FIND("*",SUBSTITUTE(B3,"\","*",LEN(B3)-LEN(SUBSTITUTE(B3,"\",""))))+1,LEN(B3))

 こちらも説明すると、最後の区切り文字「\」を探して、「*」に置き換えて、「*」から最後までを切り出す、という感じですね。「*」に置き換えるのは上記と同様にSUBSTITUTE関数を利用していますが、秀逸なのは、「最後の」を決めるところですね。ここで「LEN(B3)-LEN(SUBSTITUTE(B3,”\”,””)))」とやっていますが、全体の文字数から、「\」を取り除いた文字列の文字数を引くことで、「\」の個数を求めています。いや、最初に思いついた人、天才かも。ただ、ちょっと長くて難しいのですよねぇ。

決定版

 で、関数を使ってファイル名を取り出すもっと美しい方法はないのかと、調べていたのですけど、最初に関数でどうやるのか、と、考えていたのが間違いでした。これって、実は、ぼくはこんなに難しいことができますよ~、という自慢でしかないのでした。

 と、いうことで、反省しました。これが恐らくいちばん簡単ではやい方法です。パスを選択して、「*\」で置換を使います。これで一発解決です。ものの数秒で解決しました。

絶対パスからファイル名のみを取り出す

 説明すると、検索する文字列に指定した「*」はワイルドカードと言って、0文字以上のすべての文字に一致する、という意味になります。なので、最初の例で言うと、まず「C:\」が置換されます。置換後の文字列は未入力なので、置換される、と言っても消去されます。次に、「work\」が消去され、「tmframework\」「modebi-fw\」「TMD\」と、次々に消去されて、「MODEBI_FW_サンプルデータ.xlsx」が残ります。

まとめ

 あまり技術力にこだわり過ぎないほうがよいかも。時には柔軟な発想でやりましょう。

在宅勤務4

 今日も見に来てくださって、ありがとうございます。石川さんです。

 在宅勤務もそろそろ終わりでしょうかねぇ、と、思っていましたが、ちょっと延長しそうな気配ですね。緊急事態宣言は解除されそうな雰囲気ですが、一斉に勤務開始、というふうにはならなさそうです。もうちょっと自宅の作業スペースの効率をよくしましょうね。と、いうことで、、、

 ぼくはふだんノートパソコンでお仕事しています。ノートパソコンは机の上なので、作業するのに姿勢がちょっとうつむき加減になって、ずっと作業を続けていると、首が痛くなるのですよね。で、書籍を積み上げて、その上にノートパソコンを置いて作業していました。少しの期間だから、と、思ってそのまま使っていましたが、そろそろちゃんとしようと思いまして、スタンドを用意しました。

ダンボール製ノートパソコンスタンド

 ジャジャーン、ちゃんとしました!(笑)

ダンボール製ノートPCスタンド
ダンボール製ノートPCスタンド

 はい、自作しました。市販のスタンドを買ってくる、というのでもよかったのですけど、探すのが億劫で、またネット通販だと時間がかかったり、届いたモノとイメージが違ったりするとやだなぁ、と、思いまして、自作することに決めました。と、言ってもダンボール工作ですけどね。ダンボールなら、イマイチでも捨てて作り直せばいいですし、エコですよね。

 ちょっと高めにして、姿勢よく作業できるようにしてみました。もしかしたら高すぎるかもしれませんが、しばらく使ってみて、ダメだったら、1センチずつ切って調節しようと思っています。

つくりかた

 つくりかた、知りたい人なんているか?という自問自答しながら、どうやってつくったか書いておきます。

 椅子に座って、これくらいの高さにしたい、というところへ持って行って、机からノートパソコンの距離を測りました。手前は20センチメートルですね。ぼくの場合、ノートパソコンを一番開いた状態にしたかったので、斜めに置くことになるので、後ろの方も高さを計測しました。こちらは30センチメートルです。そして、斜めにしたときのおおよその距離を測ると20センチメートルだったので、これで設計図を作ってみました。

手書きの設計図
手書きの設計図

 はい、手書きです。(笑)しかも、テキトーな上に、間のつなぎ部分の図面はありません。

 奥行きの20センチはちょっと不安だったので、5センチプラスしました。これを二枚つくります。あと、30×7の長方形の紙をつなぐ部分に用意して、つなぎにしました。切込みを台の下部へ前から20センチメートルのところと、つなぎ部分の左右から5センチメートルのところに4ミリの切れ込みをいれまして、組み立てて、終了です。

まとめ

 作業時間、およそ20分くらいでできましたので、首が痛くならないための投資としてはお安いものです。もっと早くやってればよかったですね。

GCPチュートリアル Hello World!アプリのデプロイ2

 今日も見に来てくださって、ありがとうございます。石川さんです。

 いや~、前回は、たいへんがんばりました。でも、とっても読みにくい記事になってしまったのでは、と反省いたしました。ホントのところは、まとめのあたりではもう、力尽きていたのですよねぇ。すみませんでした。

 と、いうことで、前回のホントのまとめをやりたいと思います。整理すると、以下のような感じですね。

GCPチュートリアル Hello World!アプリのデプロイ概要

GCPチュートリアル Hello World! デプロイ概要

 手順としては、プロジェクトを作成するか選んで、アプリの所属を決めます。そして、Cloud Shellと呼ばれるシェルの実行環境からコマンドラインでいろいろ実行していきます。最初に、用意されているサンプルアプリケーションをgithubリポジトリからクローンします。ま、コピーと同じですね。

 次に、試しにテスト実行してみました。そのあと、アプリを作成して、デプロイを実行。ここまででアプリが使えるようになるために必要な手順が出そろいました。アプリを作りたい人は、コピーしてきたリポジトリをどんどん追加修正していけば、オッケー、という感じでしょうか。

 チュートリアルの後半は、アプリの状態を確認することと、無効化すること、さらに、そのリソース管理としてプロジェクトをシャットダウン(削除)するところまで実行してみました。これでぼくも立派なプロジェクト管理者、アプリ作成者ですね。

そして、、、

 前回は力尽きて見逃していたのですけど、チュートリアルの最後に「まとめ」がありました。ステップ8/8ですね。

ステップ8/8前半

 丁寧にも、このチュートリアルを完了した人たちに向けて、次に何ができるか、というものも示してくれていました。

ステップ8/8後半

 そう、Google Cloud SDKをダウンロードしたら、ローカルで開発ができるようです。他にDjango(Pythonの別のWebフレームワーク)を利用したアプリを作成できるし、ウェブアプリのビルド、ということでこのチュートリアル以上のことも、学べそうです。

まとめ

 PythonのWebフレームワーク、FlaskやDjangoを使った開発がGCPで簡単に実現できるのですね。これがあれば、お客さまへ、いろいろな提案ができそうです。そういえば、サービスの名前を気にしていませんでしたが、ishikawasekkei.comのサブドメインにする方法とか、どうするのでしょうね。また機会があれば調べてみたいと思います。

GCPチュートリアル Hello World!アプリのデプロイ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、何となくGoogle Cloud Platformコンソールを眺めていると、ふと、目に留まるものがありました。

GCPコンソール

 そうそう、Google Cloud Platformのスタートガイドにあった「Hello Worldアプリをデプロイ」というメニューです。先日「デプロイってなに?」と尋ねられたとき、ウェブサーバーにプログラムを配備して使えるようにすること、だけど、それでホントにあってるかなぁ、と、心配になったのでした。ここに、ありましたよ!デプロイが!!

 スタートガイドなので、初心者にも優しいはず。GCP、契約はしているんだけど、このWordPressのホームページ以外、ほとんど活用できていないのですよねぇ。会社を始めたときは、ぼくはGCPのプロフェッショナルになろう、と、思っていたのですけどねぇ。おぼれそうになって、しばらくお休みしていました。と、いうことで動かして見ました。

 クリックすると右側に概要が現れます。こんな感じです。

チュートリアルApp Engineのクイックスタート

 はい、では、右下の「開始」をクリックして始めたいと思います。でもその前に、チュートリアルの左にある「←」をクリックして見ます。

いろいろなチュートリアルメニュー

いろいろなチュートリアルがあるのがわかりました。今回は「App Engineを試す」ということでもう一度クリックして戻ります。

Hello Worldに利用する言語が選択できます。

 今回はPythonを選択します。いったん戻らないとGoが自動的に選択されてしまうので、違う言語でチュートリアルをやりたい方は、同じ手順でやってくださいね。上のPythonを選ぶとまた元の画面に戻ります。あと、環境は二つあって、上の「Python」は「スタンダード環境」下は「フレキシブル環境」と、異なります。違いについては、こちらに記載があります。自由度の高いシステムの場合は「フレキシブル環境」を選択する必要がありますが、今回は「スタンダード環境」で問題ありませんので、こちらを選択します。

 では、チュートリアルなので、そんなに難しく考えず、とにかく「開始」です!やってみて、初めてわかることって、けっこうあるんです。

プロジェクト設定

 お、プロジェクトを選ぶか、新しく作成するのですね。既存のプロジェクトがチュートリアルで汚れる(?)のはイヤなので、新しくプロジェクトを作りましょう。「新しいプロジェクトを作成」のところをクリックしてみます。

新しいプロジェクト作成画面

 なんと、新しいプロジェクト作成画面が開きました。お試しで作った時はプロジェクト名を変えて作成したのですけど、どうせ後で削除するプロジェクトなので、このまま「作成」をクリックします。

通知が表示されました。

 はい、通知が表示されて「My Project 60925」が作成されました。あ、前回と、、、前々回のプロジェクト作成履歴が表示されていますね。(笑)実は、二回も実験しています。慣れるまで、何回かやる必要があるかと思って、今回、三度目の正直ですね。

プロジェクト設定に新しいプロジェクトが選択されました

 プロジェクト設定の画面、新しいプロジェクトがセットされた状態になりました。「次へ」をクリックして進みましょう。

Cloud Shellを使用する

 次に、Cloud Shellを使う、ということですね。Cloud Shellとはいったい何なのか、全体を見渡すと、ありました。このアイコンですね。

Cloud Shellをアクティブにするボタン

 ボタンが見つけられましたので、クリックして見ます。何やらプロビジョニングしています、、、というのがしばらく表示された後、でました!

CLOUD SHELL

 なるほど、クラウド上のシェルだから、CLOUD SHELLなのですね。そのまんまやん。そして、次は、サンプルコードのクローンを作成して、「Hello World」コードに移動するのですが、このクローン作成、ちょっとだけ注意が必要です。

クローンを作成するコマンド

 よく見てください。なんと、続きがあるのです。

マウスをポイントすると、水平スクロールバーが登場、後半があることが判明

 ぼくは、「git clone https://github.com/GoogleCl」と入力してEnter、エラーになったくちです。いちばん右側のアイコンでコピーできますし、その左隣のアイコンでCloud Shellにコピペしてくれます。便利ですね。コピペして、実行しましょう。

実行結果

 はい、しばらくクルクルしてから終了しました。次はチュートリアルの指示に従ってディレクトリを移動します。クローンとして作ったフォルダへ移動します。ホームディレクトリの配下にできた「python-docs-samples/appengine/standard_python3/hello_world」へ移動します。こちらもボタンのコピペで実行できますね。(追記:2021年8月29日に実行して確認したところ、ディレクトリ名が画像と異なっていました。ま、コピペすれば大丈夫ですね。)

ディレクトリを切り替え

では、チュートリアルの「次へ」ボタンを押して進みます。ステップ3/8です。

ステップ3/8の前半

 デプロイの構成、ということでmain.pyの内容表示してみます。コメントがたくさんあって、必要なところは以下の部分だけでした。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    """Return a friendly HTTP greeting."""
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

 Flaskというのは、Python用のWebフレームワークです。このスクリプトはルートにアクセスされると単に挨拶として「Hello World!」を戻します。

ステップ3/8後半

 ここでは、構成ファイルを表示ということで中身を出力してみましたが、

runtime: python37

 というふうに、ランタイムにpython37を使う、ということがわかりました。「次へ」をクリックします。(追記:2021年8月29日時点で、こちらは「python39」になっていました。時代は進みますねぇ。ちなみにチュートリアルの記載は「python38」でした。更新漏れですね。(笑))

ステップ4/8前半

 次は、アプリのテストです。virtualenvを実行して仮想環境を作成して、それを有効にする、ということですね。コピペで実行します。

virtualenv実行結果
ステップ4/8後半

 次に、pipでFlaskに関連するモジュールをインストールしてmain.pyを実行します。

pip実行結果

(追記:2021年8月29日 pip実行時、pipのバージョンが古いですよ、と、WARNINGが出力されました。アップグレードの方法が記載されていましたが、ま、放置していても問題ありませんよね。)

main.py実行結果

 次に、ウェブでプレビューボタンをクリックします。ここにありました。

ウェブでプレビューボタン

クリックします。

ウェブでプレビューボタンをクリックしたところ

 ポート8080でプレビューを選択します。すると、

Hello World!

 やっと、たどり着きました。念願の「Hello World!」です。単に「Hello World!」の文字を出力しているだけです。ふ~。これでやっと半分終了ですね。Ctrl+Cキーでアプリーションのインスタンスが終了しました。「次へ」をクリックします。

ステップ5/8 App Engineへのデプロイ

 ほう、リージョン内に、アプリを作成して、それからデプロイですね。やっとでました、デプロイです。ちょっとここまでやってみましょう。

gcloud app create実行結果

########## 2021年8月29日 追記開始 ここから  ##########
「gcloud app create」を実行したところ、Cloud Shellの承認というポップアップが表示されました。途中で作業を中断して再開したせいかもしれません。

CloudShellの承認ポップアップ

「承認」をクリックしましたが、画面上にはエラーが出ていました。どうやらうまく承認はされなかったようです。

gcloud app create実行時に発生したエラー

 現在、アクティブなアカウントが選択されていません、ということのようですので、言われるがままに、「gcloud auth login」を実行してみました。以下のようにURLが示されました。その下に「Enter verification code:」と書いてあるので、URLで表示された先で認証コードが取得できるのでしょう。

gcloud auth loginを実行したところ

 と、いうことで示されたURL(https://accounts.google.com/o/oauth2/…)をクリックすると、ブラウザの別タブに「アカウントの選択」が表示されましたので、こちらのGCPで使用しているアカウントを選択しました。

アカウントの選択

すると、以下の通りリクエストの許可を求めてきました。

アカウントのアクセスをリクエスト

「許可」をクリックしたところ、以下のとおり、ログイン用のコードが表示された画面が出てきました。

ログイン用のコード

これをコピーして「Enter verification code:」のところへ貼り付けます。すると、ログインできた、というメッセージが表示されました。

verification codeを入力したところ

ここで改めて「gcloud app create」を実行したところ、上記の「gcloud app create実行結果 」の結果が表示されました。
########## 2021年8月29日 追記終了 ここまで  ##########

 おっと、「Please enter your numeric choice:」と言われています。あなたのApp Engineアプリケーションを置くリージョンを選んでください、ということで、まあ順当に[1]番でしょうねぇ。

リージョン選択後の画面

 リージョンを選択すると、10秒ほどクルクルして完了しました。「Success!」と言ってますね。次に「gcloud app deploy」を使ってあなたの最初のアプリをデプロイしてください、と言ってますね。チュートリアルの方はもうちょっとたくさん記述があって「gcloud app deploy app.yaml \ –project t-system-277912」と書いてありますね。チュートリアルの方を実行してみましょう。

gcloud app deploy app.yaml \ –project t-system-277912実行後

 おっと、続けますか?と、聞いてきています。もちろん続けますよ。「Y」を入力してエンター。

5分後、デプロイ完了です

 5分ほど経ったでしょうか、デプロイが完了いたしました!

 「アプリにアクセスする」をクリックして、見事、「Hello World!」が出力されました。

デプロイされた「Hello World!」アプリ

 「次へ」をクリックして6/8へ行きましょう!

ステップ6/8

 次は、アプリのステータスを表示する、ということですね。[App Engine]を選択するのですね。

App Engineを選択します

App Engineをクリックすると次の画面があらわれました。

App Engineのダッシュボード

 「次へ」をクリックします。

ステップ7/8

 設定ページに行って、アプリケーションを無効にすれば、課金されなくなるそうです。

設定画面

 「アプリケーションを無効にする」をクリックしてみます。

「アプリケーションを無効にする」をクリックしたところ

 おお、無効にしますか?と、聞いてきました。アプリIDを入力しないと無効にならないようです。間違って無効にしちゃった、という事故が起きにくい仕組みになっているようです。アプリIDを入力して、「無効にする」をクリックします。

アプリケーションを無効にしたところ

 次は、「プロジェクトを削除」ですね。[IAMと管理]を探します。

IAMと管理

 「IAMと管理」をクリックします。

IAMと管理をクリックしたところ

 次に、「リソースを管理」をクリックします。

リソースの管理画面

 今回作成した「My Project 60925」をチェックして「削除」をクリックします。

削除の確認画面

 おお、削除の確認画面ですね。ここでもプロジェクトIDを入力しないといけないようです。間違って違うプロジェクトを削除しにくい仕組みですね。それに30日間の猶予もあるのですね。プロジェクトIDを入力して「シャットダウン」をクリックします。削除なのに「シャットダウン」なのですねぇ。ちょっと不思議な感じがします。

シャットダウン後のメッセージ

 「OK」クリックしたところ、プロジェクトが消えました。

まとめ

 いや~、チュートリアルを順番にやっただけですが、長くなっちゃいましたね。ここまでお読みくださってありがとうございます。おつかれさまでした。

Python tkinter GUI プログラミング ドロップダウンボックス

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、バーコードを作るプログラムサンプルを作成しました。その時に、バーコードの種類を選べるようにドロップダウンボックスを用意して、選べるようにしようかな、と思いました。ところが調べてみるとドロップダウンの機能はOptionMenuとComboboxの二種類ありました。ぼくの思っていたドロップダウンはComboboxの方でしたが、ちょっと調べたものをまとめておきたいと思います。

出来上がりイメージ

 はい、今回のプログラムの出来上がりイメージはこんな感じです。上に、OptionMenu、下にComboboxを配置しました。選択肢は月の和名にしてみました。

ドロップダウンボックスのサンプル

ソースコード

 ソースコードは以下の通りです。

import tkinter as tk
import tkinter.ttk as ttk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ドロップダウンサンプル")
        
        months = ["睦月","如月","弥生",
                  "卯月","皐月","水無月",
                  "文月","葉月","長月",
                  "神無月","霜月","師走"]
        
        option_label = tk.Label(self, text="OptionMenu:")
        option_label.grid(row=0, column=0, padx=10, pady=10)

        self.ov = tk.StringVar()
        self.ov.set("弥生")
        self.o = tk.OptionMenu(self, self.ov, *months, command=self.optionmenu_selected)
        self.o.grid(row=0, column=1, padx=10, pady=10)
        
        combo_label = tk.Label(self, text="Combobox:")
        combo_label.grid(row=1, column=0, padx=10, pady=10)

        self.c = ttk.Combobox(self, values=months)
        self.c.grid(row=1, column=1, padx=10, pady=10)
        self.c.current(2)
        self.c.bind("<<ComboboxSelected>>", self.combobox_selected)
        
    def optionmenu_selected(self, event):
        print(event, self.ov.get())

    def combobox_selected(self, event):
        print(self.c.get())

if __name__ == '__main__':
    application = Application()
    application.mainloop()

プログラムの説明

 最初のポイントは19行目のOptionMenu()のインスタンスの作成方法ですね。第一引数のselfはよいとして、第二引数は、テキストの変数を指定します。第三引数以降に選択値を書いていきます。今回は、リストで定義しましたので、アスタリスク「*」を付けることでアンパックしています。commandで値が変更されたときの処理を指定することができます。18行目で初期値をセットしています。

 次のポイントは25行目のCombobox()のインスタンス作成のところでしょう。27行目のself.c.current(2)で、3個目の要素を初期値にしています。値が変更されたときのイベントを定義するために、28行目のself.c.bind("<<ComboboxSelected>>", self.combobox_selected)の中で疑似イベント<<ComboboxSelected>>を使って処理を指定しています。

 現在値の取得は、それぞれ31行目のself.ov.get()と34行目のself.c.get()というふうに記載します。

まとめ

 tkinterはもともとUNIXベースで作られていたのだと思います。OptionMenu()の選択肢は、UNIXユーザになじみのあるユーザーインターフェースですが、tkとttkの関係から、こちらの方が先に作られていたようです。Windowsユーザには、Comboboxの方がなじみがありますよね。これで、いくつかの選択肢があっても大丈夫ですね。

Python tkinter GUI Programing バーコード

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、おしごとでバーコードを出力することがあって、ちょっと苦労しました。code128のバーコードを出力しなきゃいけなかったのですけど、このコード、スタートコードがあって、データ部分があって、チェックディジットのコードがあって、ストップコードがある、という風な構造になっていました。これだけ調べて出し方がわかるまでにも結構時間がかかりましたよ~。バーコード、難しいですねぇ。

 もっと簡単にできる方法を調査すべく、Python tkinterではどうやってバーコードを出すのかな、ということで調べました。まったくやり方がわからないので、Python barcodeでグーグル先生に聞いてみました。便利な世の中ですねぇ、すぐにPython-barcodeを使えばよいことがわかりました。

 出力結果を出すまで紆余曲折ありましたが、なんとか出力することができるようになりました。

できあがりイメージ

Barcodeを出力してみました

ソースコード

 ソースコードは以下の通りです。

import barcode
from barcode.writer import ImageWriter
from PIL import ImageTk
import tkinter as tk
import tkinter.filedialog as fd
import tkinter.messagebox as mb

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Tkinter barcode test")
        
        self.label = tk.Label(self, text="Input code128:")
        self.label.grid(row=0, column=0)
        self.barcode = tk.StringVar()
        self.barcode.set('Python tkiner')
        self.entry = tk.Entry(self, textvariable=self.barcode,width=50)
        self.entry.grid(row=0, column=1, columnspan=2)
        self.show_button = tk.Button(self, text="Show a barcode")
        self.show_button.bind("<Button-1>", self.create_image)
        self.show_button.grid(row=1,column=1)
        self.save_button = tk.Button(self, text="Save a barcode image")
        self.save_button.bind("<Button-1>", self.save_image)
        self.save_button.grid(row=1,column=0)
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.grid(row=2,columnspan=3,sticky=tk.NSEW)
        self.update()
        self.create_image(None)
        
    def create_image(self, event):
        data = self.barcode.get()
        if not data:
            return
        try:
            code128 = barcode.get('code128', data, writer=ImageWriter())
        except barcode.errors.IllegalCharacterError as e:
            mb.showerror("Error", e)
            return
        image = code128.render()
        photo = ImageTk.PhotoImage(image)

        self.canvas.delete(tk.ALL)
        self.canvas.create_image(self.canvas.winfo_width()//2,self.canvas.winfo_height()//2,image=photo)
        self.image = photo
        
    def save_image(self, event):
        data = self.barcode.get()
        if not data:
            return
        try:
            code128 = barcode.get('code128', data, writer=ImageWriter())
        except barcode.errors.IllegalCharacterError as e:
            mb.showerror("Error", e)
            self.update_idletasks()
            return
        filename = fd.asksaveasfilename()
        if filename:
            code128.save(filename)
        

if __name__ == '__main__':
    application = Application()
    application.mainloop()

詳細

 Python-barcordは標準モジュールではありませんので、インストールが必要でした。ぼくの環境はAnacondaですので、通常はcondaを使ってインストールするのですが、condaで管理しているところにはセットアップされていなかったので、インストラクションどおり、pipを利用しました。

pip install python-barcode

 インストールは、上記のコマンド一行、これだけです。簡単ですねぇ。一瞬で終了しました。

 インストールが完了しましたので、インストラクションどおりに動作するかどうか確認してみました。よくわからないことにチャレンジするときは、ひとつひとつ動作確認していきます。まずは、説明通り動作するか、書かれてあることを試してみます。そして、自分が実現したいことで利用可能なのかどうか、ちょっとずつ、試していくことになります。地道な作業です。

 今回は、ちょっとずつ試したことは割愛して、ソースコードの説明をしたいと思います。

 まずは、入力されたコードをもとに、バーコードを出力できるようにしたいと思います。バーコード出力先には、キャンバスを使うことにしました。ラベルやボタンでもイメージをセットできるようです。__init__では、これらのtkitnerのウィジェットを組み合わせ枠組みを組み立てています。今回の主ポイントではないので割愛します。

 バーコードのデータは35行目のcode128 = barcode.get('code128', data, writer=ImageWriter())で作成しています。第一引数の’code128’はcode128形式のバーコードを作ることを指定しています。dataは作成されるバーコードのもととなるデータで、これはEntry()で入力されたデータです。15行目で定義したStringVar()のインスタンスを17行目のEntry()インスタンス作成時のパラメータtextvariableへセットすることで、Entry()の内容が変わると自動的にStringVar()インスタンスの値も更新されるようになります。writerを指定しない場合は、svg形式のイメージができるようです。今回はtkinterで利用可能なpngへ変更するためにImageWriter()を指定しました。

 39行目のrender()で作成したイメージデータを出力しています。40行目のphoto = ImageTk.PhotoImage(image)この部分ですが、ImageTK.PhotoImage()は、tkinterで使えるようイメージ変換してくれます。

 で、ポイントは43行目、キャンバスの中央にイメージを作成しています。create_image()メソッドです。指定する座標は作成するイメージの中心点ということだったので、キャンバスの幅と高さを取得してその半分を座標にしました。やっとイメージがキャンバス上に表示されるようになりました。しかし!動かしてみるとわかりますが、これだけではイメージ出力されません。その次の44行目がミソです!イメージをクラスインスタンスに代入しています。これがないと、ローカル変数の値は、ガーベージコレクト、要するにメモリのお掃除対象になってしまって、処理が終わったら消えてしまうのです。最初はこれがなくて、バーコード表示されなかったのですよねぇ。

まとめ

 いや~、今回はがんばりました!バーコード作成は、需要があると思うのですけど、日本語の情報が少ないのですよね。誰かのお役に立つと、うれしいです。

Python tkinter GUI プログラミング Treeview

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、Treeviewについて調べてプログラミングしてみました。Treeviewには二種類の見せ方があって、headingstreeです。それぞれ上下に実装してみました。

できあがりイメージ

Treeviewの実装サンプル(showの二つのモード、上がheadings、下がtree)

ソースコード

import tkinter as tk
import tkinter.ttk as ttk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ttk Treeview Widget example headings/tree")
        
        lf1 = tk.LabelFrame(self, text="headings")
        lf1.grid(row=0,column=0, padx=10,pady=10)
        headings = {"#1":"名前", "#2":"型", "#3":"サイズ"}
        self.treeview = ttk.Treeview(lf1, columns=headings.keys(), show="headings")
        self.treeview.pack(side=tk.LEFT, fill=tk.BOTH, expand=True,padx=10,pady=10)
        self.scroll = ttk.Scrollbar(lf1, command=self.treeview.yview)
        self.treeview.configure(yscroll=self.scroll.set)
        self.scroll.pack(side=tk.LEFT, fill=tk.BOTH, pady=10)
        
        for key, item in headings.items():
            self.treeview.heading(key, text=item)
            
        self.treeview.column("#1", width=500)
        
        self.treeview.insert("",tk.END, values=("head_information","dict","3"))
        self.treeview.insert("", tk.END, values=("tooltip_text","str","1"))
        
        lf2 = tk.LabelFrame(self, text="tree")
        lf2.grid(row=1,column=0, stick=tk.W, padx=10, pady=10)
        style = ttk.Style(lf2)
        style.configure('Treeview', rowheight=30)
        self.treeview2 = ttk.Treeview(lf2, show="tree")
        self.treeview2.heading("#0", text="tree view version", anchor=tk.W)
        self.treeview2.pack(padx=10, pady=10)
        parent1 = self.treeview2.insert("", tk.END,text="head_information")
        self.treeview2.insert(parent1, tk.END, text="dict")
        self.treeview2.insert(parent1, tk.END, text="3")
        parent2 = self.treeview2.insert("", tk.END, text="tooltip_text")
        self.treeview2.insert(parent2, tk.END, text="str")
        self.treeview2.insert(parent2, tk.END, text="1")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

詳細説明

 9行目と26行目でLabelFrame()を作成しています。それぞれheadings用と、tree用に準備しました。12行目でshow="headings"オプションをつけたTreeviewを作成しています。columnsオプションでカラムを指定しています。

 19行目のheadings()メソッドで、項目のヘッダ部分のテキストを設定しています。21行目でヘッダ項目の幅をセットしています。

 headingsを指定することで、このような表形式でデータを扱うことができます。

 30行目で、show="tree"オプションを付けたTreeviewを作成しています。フォルダ階層などのツリー形式のデータを表現することができます。

まとめ

 headingsオプションを使ったTreeviewは、データベーステーブルからデータを取得して一覧を作成するときなどに、使えそうですね。treeオプションの方は、フォルダ階層以外にもプログラム構成や、組織構造など、幅広く使えそうです。

Python tkinter GUIプログラミング ツールチップ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回は、tkinterでツールチップを出力する方法を調べてみました。いい感じにできたのでまとめてみます。もともと、ホバーイベントって、どうやるのかなぁ、というのが発端でした。ホバーとは、マウスを同じところでじっとしていると発生するイベントです。tkinterのイベントの中にホバーは見当たらなかったので、たぶん、そんなイベントは存在しないのだと思います。

出来上がりイメージ

ソースコード

import tkinter as tk

class ToolTip():
    def __init__(self, widget, text="default tooltip"):
        self.widget = widget
        self.text = text
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Motion>", self.motion)
        self.widget.bind("<Leave>", self.leave)
        self.id = None
        self.tw = None

    def enter(self, event):
        self.schedule()
    
    def motion(self, event):
        self.unschedule()
        self.schedule()
    
    def leave(self, event):
        self.unschedule()
        self.id = self.widget.after(500, self.hideTooltip)
    
    def schedule(self):
        if self.tw:
            return
        self.unschedule()
        self.id = self.widget.after(500, self.showTooltip)
    
    def unschedule(self):
        id = self.id
        self.id = None
        if id:
            self.widget.after_cancel(id)
    
    def showTooltip(self):
        id = self.id
        self.id = None
        if id:
            self.widget.after_cancel(id)
        x, y = self.widget.winfo_pointerxy()
        self.tw = tk.Toplevel(self.widget)
        self.tw.wm_overrideredirect(True)
        self.tw.geometry(f"+{x+10}+{y+10}")
        label = tk.Label(self.tw, text=self.text, background="lightyellow",
                         relief="solid", borderwidth=1, justify="left")
        label.pack(ipadx=10)

    def hideTooltip(self):
        tw = self.tw
        self.tw = None
        if tw:
            tw.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Tooltip test")
    root.geometry("400x100")
    button = tk.Button(root,text="test button")
    button.pack()
    tooltip = ToolTip(button)
    button2 = tk.Button(root, text="next button")
    button2.pack()
    tooltip_text = "ツールチップをセットすることができます。\n改行コードを入れることで複数行になります。"
    tooltip2 = ToolTip(button2, tooltip_text)
    root.mainloop()

詳細説明

 今回は、汎用的に利用できる、ウィジェットではないToolTipクラスを作成してみました。利用方法は、61行目と65行目の通りです。それぞれ、ボタンのツールチップを設定しています。インスタンス作成時にウィジェットとツールチップに表示する文字列を渡します。

 ポイントは、ウィジェットにマウスポインタが入った時の<Enter>イベントと出て行った時の<Leave>イベント、それとマウスポインタが入っている間に発生する<Motion>イベントでそれぞれ、ツールチップの表示をスケジュールする、ツールチップの消去をスケジュールする、動いている間はスケジュールをやり直す、というところでしょうか。

 <Enter>イベントが発生したときにスケジュールするのに、28行目の「self.id = self.widget.after(500, self.showTooltip)」のafter()メソッドを使用しています。これで500ミリ秒後に、self.showTooltip()を実行してね、とスケジュールしています。これにより、イベント発生後に何もしなければ、ツールチップが表示されるようになります。

 <Leave>イベントが発生したときは、既に表示されているはずのツールチップを消去する必要がありますので、ここでも500ミリ秒後に消去するメソッドを呼び出すようスケジュールしています。これが28行目の「self.id = self.widget.after(500, self.hideTooltip)」になります。

 ツールチップ自体は、Toplevelを使って表示しています。43行目の「self.tw.wm_overrideredirect(True)」を呼び出すことによって、ウィンドウマネージャがこのウィジェットを無視するようにします。要するに、ウィンドウタイトルや最小化、最大化、ウィンドウを閉じるボタンなどをセットしないようになります。

 表示位置を決めるために、41行目の「self.widget.winfo_pointerxy()」を利用しています。ポインタの位置にウィンドウを表示すると、表示したウィンドウにポイントすることになるので<Leave>イベントが発生し、これによりウィンドウが消去される、という循環が発生してしまうので、位置を10ピクセルずつ少しずらして表示するよう工夫しました。

まとめ

 ホバーというイベントはなくても、ツールチップを出すことができました。これでマウスがじっとしているときに何かするプログラムが作れるようになりましたね。

Python GUIプログラミング Canvas Text入力

 今日も見に来てくださって、ありがとうございます。石川さんです。

 TkinterCanvasでテキストを入力することができるかどうか、ということで調べてみました。先日、Canvasのメソッドのcreate_text()でテキストを描画することはできたのですが、いろいろと調査した結果、このテキストを編集することができる、ということがわかりましたので、まとめます。

実行イメージ

キャンバス上のテキストを編集可能にしました

 マウスクリックでテキストを選択、矢印キーでカーソル位置を移動、HomeキーやEndキーが使えます。Enterキーで確定、Escキーで取り消し、タブキーで次の項目へ移動、シフトキーと移動キーで部分選択が可能です。

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas text edit")
        self.geometry("400x280")
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.tag_bind("editable","<Button-1>", self.clicked)
        self.canvas.tag_bind("editable","<Key>", self.do_key)
        
        self.focused_item = None
        self.focused_item_text = None
        
        self.canvas.create_text(20,80, anchor=tk.NW, text="このテキストは修正可能です。", tags=("editable",))
        self.canvas.create_text(20, 120, anchor=tk.NW, text="このテキストも修正可能です。", tags=("editable",))
        self.canvas.create_text(20, 160, anchor=tk.NW, text="このテキストも修正可能です。", tags=("editable",))

    def clicked(self,event):
        if self.canvas.type(tk.CURRENT) == "text":
            prev_item = self.focused_item
            self.canvas.focus_set() 
            self.canvas.focus(tk.CURRENT)
            x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
            self.focused_item = self.canvas.find_overlapping(x, y, x, y)
            if prev_item == self.focused_item:
                x = self.canvas.canvasx(event.x)
                y = self.canvas.canvasy(event.y)
                if event.state&1:
                    index_current = self.canvas.index(tk.CURRENT, tk.INSERT)
                    index_selected = self.canvas.index(tk.CURRENT, "@%d,%d" % (x, y))
                    if index_current <= index_selected:
                        self.canvas.select_from(tk.CURRENT, index_current)
                        self.canvas.select_to(tk.CURRENT, index_selected)
                    else:
                        self.canvas.select_from(tk.CURRENT, index_selected)
                        self.canvas.select_to(tk.CURRENT, index_current - 1)
                else:
                    self.canvas.icursor(self.focused_item, "@%d,%d" % (x, y))
                    self.canvas.select_clear()
            else:
                self.canvas.select_from(tk.CURRENT, 0)
                self.canvas.select_to(tk.CURRENT, tk.END)
            if self.focused_item:
                self.focused_item_text = self.canvas.itemcget(self.focused_item,"text")
            else:
                self.focused_item_text = None

    def do_key(self, event):
        if event.keycode == 229: # IME入力中
            return
        item = self.canvas.focus()
        if item:
            current_index = self.canvas.index(item,tk.INSERT)
            if event.keysym == 'Right':
                new_index = current_index + 1
            elif event.keysym == 'Left':
                new_index = current_index - 1
            elif event.keysym == 'End':
                new_index = self.canvas.index(item,tk.END)
            elif event.keysym == 'Home':
                new_index = self.canvas.index(item,0)
            elif event.keysym == 'BackSpace':
                selection = self.canvas.select_item()
                if selection:
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                else:
                    if current_index > 0:
                        self.canvas.dchars(item, current_index - 1)
                return
            elif event.keysym == 'Delete':
                selection = self.canvas.select_item()
                if selection:
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                else:
                    self.canvas.dchars(item, current_index)
                return
            elif event.keysym == 'Tab':
                items = self.canvas.find_withtag("editable")
                if items:
                    index = items.index(item)
                    if index + 1 == len(items):
                        next_item = items[0]
                    else:
                        next_item = items[index + 1]
                    self.canvas.focus(next_item)
                    self.canvas.select_from(next_item,0)
                    self.canvas.select_to(next_item,tk.END)
                return
            elif event.keysym == 'Return':
                self.canvas.select_clear()
                self.canvas.focus("")
                self.focused_item = None
                self.focused_item_text = None
                return
            elif event.keysym == 'Escape':
                self.canvas.itemconfig(item, text=self.focused_item_text)
                self.canvas.select_clear()
                self.canvas.focus("")
                self.focused_item = None
                self.focused_item_text = None
                return
            elif event.keycode in (16, 17): # Shift, Ctrl
                return

            if event.char >= ' ':
                selection = self.canvas.select_item()
                if selection:
                    new_index = self.canvas.index(item, tk.SEL_FIRST) + len(event.char)
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                    self.canvas.select_clear()
                else:
                    new_index = current_index + len(event.char)
                self.canvas.insert(item, tk.INSERT, event.char)

            if event.state&1: # Shift Key
                selection = self.canvas.select_item()
                if selection:
                    if self.canvas.index(item,tk.SEL_LAST) >= new_index:
                        self.canvas.select_from(item, new_index)
                        self.canvas.select_to(item, tk.SEL_LAST)
                    else:
                        self.canvas.select_from(item, tk.SEL_FIRST)
                        self.canvas.select_to(item, new_index - 1)
                    self.canvas.icursor(item, new_index)
                else:
                    self.canvas.select_from(item, current_index)
                    if current_index < new_index:
                        self.canvas.select_to(item, new_index - 1)
                    else:
                        self.canvas.select_to(item, new_index)
                    self.canvas.icursor(item, new_index)
            else:
                self.canvas.icursor(item, new_index)
                self.canvas.select_clear()
                

if __name__ == "__main__":
    application = Application()
    application.mainloop()

 ソースコード、めっちゃ長くなってしまいました。もっと短くしたいですよねぇ。

解説

 いやー、今回のやりたいこと、割と基本的なことばかりなので、もっと簡単に実装できるのかと思っていました。それが、意外や意外、時間もかかったし、ソースコードも長くなって、ブログで紹介するのを躊躇するほどのボリュームになってしまいました。(あと、実は、バグも残ってるんですよ。ここだけの話。全部さっき気づきましたが、シフトキー押しながら大文字とか記号を入力すると、なぜか選択されちゃったり、他にもCtrl+Vで張り付けられなかったり、ファンクションキーを押すとUnboundLocalErrorがraiseしたりとか、入力をキャンセルしようとして選択されていないキャンバス上のどこかをクリックしても反応しなかったりと、いろいろ足りないことがありましたので、ブログの更新を優先することにして、そのあたりの実装は、今回はあきらめました。ちょっと整理が必要ですね。)

 今回の一つ目のポイントは11、12行目のtag_bind()です。Canvas上の要素は、連番で識別される、idか、ユーザーが定義したタグという概念で管理することができるようです。このタグにイベントをバインドする(割り当てる)ことができるのがこのtag_bind()です。idだと一つの要素にのみ影響することができますが、タグを使うことで複数の要素に対して同時に影響を及ぼすことが可能になります。今回はここで、editable(編集可能)というタグを定義しました。

 16~18行目では、create_text()実行するときにこのタグをセットしています。これでクリックされたときのイベント<Button-1>と、キー入力されたときのイベント<Key>に対する処理が割り当てられます。

 20行目からは、クリックされたときのイベントの処理になります。ソースコード上でうまく表現できていないのですけど、最初のテキストを選択したとき、と、選択されたテキストのカーソル位置を変更するとき、の、二種類の処理が混ざっています。いろいろ編集された後にEscapeキーを押されて元に戻すためのデータもこのタイミングで保持するようにしています。クリックしたときと前にクリックしたときの項目が同じならカーソル位置の変更、そうでないなら最初のテキストの選択、という風に判断しています。

 50行目から、キー入力されたときのイベントの処理です。このメソッド、長すぎますね!何とかせんかーい、と、言いたくなります。自分にですけど。いろいろと継ぎ足し継ぎ足しして作っていったのが、よくわかります。51行目の「if event.keycode == 229:」ですが、なんと、IMEで日本語入力中、つまり返還前のキー入力は、このkeycodeが229としてイベントが発生してきました。IMEの入力に対する処理は不要なので、無視するように変更しました。ドキュメントのどこかに書いてあればよいのですけど、見つけられませんでした。でも、これでよいですよね?どなたか詳しい方、教えてください♪

 それ以降は、おおよそ何をやっているかというと、カーソルの移動に対応しております。キー入力で右に行ったり左に行ったりするのも、自動では対応されていないので、自分でコーディングする必要があるのですね。もちろん、HomeキーやEndキー、Backspaceキー、Deleteキーの動きも、自分でコーディングする必要がありました。それぞれの移動時にShiftキーが押されていた時は選択したいのですけど、それも当然自分でコーディングする必要がありました。Entryウィジェットなどは、ちゃんと処理されているのですが、Canvas上の項目については定義されていません。これは、面倒ですが、、、動作を自由にカスタマイズができる、ということですね!

 91行目、Returnキーを入力したときに、確定、97行目、Escapeキーを入力したときに入力をキャンセルするように実装してみました。そして、104行目は、Shiftキー、Ctrlキーが単独で押されたときのイベントですね。後続の処理をしないようにしました。

 ちなみに、シフトキーが押された状態かどうか、というのは、117行目のif event.state&1:で判定しています。ステータスの1ビット目がセットされてくるようです。これもドキュメントに書かれているところが見つけられなかったのですよねぇ。

まとめ

 Canvascreate_text()メソッドで、テキスト入力項目を作る、というのはちょっとハードルが高い、ということがわかりました。もうちょっと整理して、共通部品の作り方を検討した方がよさそうです。