Tkinterでシュルテ・テーブルをつくる リファクタリング編

 今日も見に来てくださってありがとうございます。まだ、朝は肌寒いですねぇ。

 さて、予言通り、先日のスクリプトをリファクタリングしたいと思います。ポイントは、まず、Frameを継承しているところですね。次は、コンスタント値を定義しているところ、そして、大枠とテキストをリフレッシュしているところを分離するところでしょうか。ちょっとやってみたいと思います。

Frameの変更

 まずは、単純にFrameをTkに置き換えてみます。そして、実行。

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 73, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    super().__init__(master)
  File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk

(base) C:\work\tkinter_example>

 思った通り、エラーがでましたが、、、まずは、Tkの初期化、__init__()のところですね。アプリケーションレベルの初期化になりますので、masterをパラメータで受け取る必要はありません。master関連を削除していきたいと思います。そうすると、タイトルの設定も、self.master.title("Schulte Table")ではなくて、self.title("Schulte Table")と変更する必要がありますね。あと、self.pack()となっている部分もFrameではなくなったので、削除します。あとは、importからもFrameを削除しましょう。これだけでも__init__()が随分とスッキリしましたね。

修正前の__init__()

    def __init__(self, master=None):
        if master == None:
            master = Tk()
        super().__init__(master)
        master.title("Schulte Table")
        self.master = master
        self.pack()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

修正後の__init__()

    def __init__(self):
        super().__init__()
        self.title("Schulte Table")
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

 実行してみたところ、またしてもエラーが発生しました。

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 73, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    super().__init__(master)
  File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 69, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    self.create_widgets()
  File "SchulteTable2.py", line 38, in create_widgets
    command=self.master.destroy)
AttributeError: 'NoneType' object has no attribute 'destroy'

(base) C:\work\tkinter_example>

 ええと、destroyという属性はありませんよ、と言われていますね。destroyTkの属性でしたね。Frameを使っていたから、その親のmasterから呼び出していたのでした。なので、ここのself.master.destroyは、self.destroyに変更します。再度実行してみますと、画面はバッチリ表示されましたが、クリックするとエラーがでました。

(base) C:\work\tkinter_example>python SchulteTable2.py
Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "SchulteTable2.py", line 43, in redraw_text
    messagebox.showinfo("結果",f"かかった時間は、{self.elapse_time:0.2f}秒です。")
  File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 2101, in __getattr__
    return getattr(self.tk, attr)
AttributeError: '_tkinter.tkapp' object has no attribute 'elapse_time'

 ほお、elapse_timeがありません、とおっしゃいますか。なるほど、self.textが存在しないので、redraw_textがうまく動作していないようです。なぜ、text__init__()で初期化しなかったのでしょうね。ちょっと謎ですが、過去のことは追求してもわかりません。self.textに変更して__init__()の中へ移動して初期化するよう変更します。

 初期化の問題は解決できたようですが、挙動がおかしくなりました。クリックしてスタートするのは問題なさそうです。ただスタートしたあとに再度クリックしたら停止するはずなのですけど、クリックする位置によっては、停止した後すぐにまたスタートしてしまいます。イベントのバインドに問題があるようです。挙動から考えると、Tkを継承したトップレベルでバインドして、さらにその子供のウィジェットにもバインドしたときに、イベントに対する処理を二度実行してしまっているようです。Frameの場合はそんなことなかったのに、不思議ですね。今回は、トップレベルのイベントで充分ですので、個々のウィジェットのバインドは削除しましょう。

 そして、classのすぐ下に定義してある変数を何とか整理したいです。名前空間がモジュール内にあるので、クラス定義の外に出してしまいましょう。そうすると、「self.」を書かなくてよくなります。それでもまだ横に長いのと、わかりやすさのために値をいったん変数に入れて、後で見たときに少し確認しやすくしましょう。あと、一回も参照されていない、self.line1への代入はやめてしまいましょう。

 いろいろと改善した結果が以下のソースです。

from tkinter import Tk, Button, Canvas, Label, messagebox
from random import sample
from time import time

MARGIN = 10
DISTANCE = 50
WIDTH = 5
HEIGHT = 5
FSIZE = 25

class SchulteTable(Tk):
    def __init__(self):
        super().__init__()
        self.title("Schulte Table")
        self.text = list()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

    def create_widgets(self):
        self.info = Label(self, text="Tap anywhere to start", font=("",14))
        self.info.pack(side="top")
        width = MARGIN*2+WIDTH*DISTANCE
        height = MARGIN*2+HEIGHT*DISTANCE
        self.canvas = Canvas(self,width=width, height=height)
        self.canvas.pack(side="top")
        for i in range(WIDTH+1): #縦線
            x0, y0 = MARGIN + i * DISTANCE, MARGIN
            x1, y1 = MARGIN + i * DISTANCE, MARGIN + DISTANCE * HEIGHT
            self.canvas.create_line(x0,y0,x1,y1)
        for i in range(HEIGHT+1): #横線
            x0, y0 = MARGIN, MARGIN + i * DISTANCE
            x1, y1 = MARGIN + WIDTH * DISTANCE, MARGIN + i * DISTANCE
            self.canvas.create_line(x0,y0,x1,y1)

        self.quit = Button(self, text="QUIT", fg="red", command=self.destroy)
        self.quit.pack(side="bottom")

    def redraw_text(self, event):
        if not len(self.text):
            self.info["text"] = "Tap anywhere to STOP"
            self.time = time()
            self.after(100, self.time_update)
        else:
            for t in self.text:
                self.canvas.delete(t)
            self.text = list()
            message = f"かかった時間は、{self.elapse_time:0.2f}秒です。"
            messagebox.showinfo("結果",message)
            self.info["text"] = "Tap anywhere to start"
            return
        counter = 0
        answers = sample(range(1,WIDTH*HEIGHT+1),WIDTH*HEIGHT)
        for x in range(WIDTH):
            for y in range(HEIGHT): # 数字のテキスト描画
                x0 = MARGIN + x * DISTANCE + DISTANCE / 2
                y0 = MARGIN + y * DISTANCE + DISTANCE / 2
                ans = str(answers[counter])
                text = self.canvas.create_text(x0,y0,text=ans,font=("",FSIZE))
                self.text.append(text)
                counter+=1

    def time_update(self):
        if not len(self.text):
            return
        self.elapse_time = time() - self.time
        self.info["text"] = f"Tap anywhere to STOP:{self.elapse_time:0.2f}"
        if len(self.text):
            self.after(10, self.time_update)

if __name__ == '__main__':
    shulteTable = SchulteTable()
    shulteTable.mainloop()

 どうでしょう、少しは分かりやすくなったでしょう。もしかして、単なる自己満足でしょうか。。。

Tkinterでシュルテ・テーブルをつくる

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

 みなさん、シュルテ・テーブルというものをご存知でしょうか。先日読んだ本に、脳のトレーニングとして紹介されていました。少し前にこれをtkinterで作ったので、紹介したいと思います。

できあがりイメージ

 シュルテ・テーブルは、以下のような表です。これで、周辺視野、注意力、セルフコントロール、集中力などが鍛えられるということです。表の中心に視線を固定して、周辺視野だけを使って、1~25を順番に探すだけです。しばらく続けれていれば、12秒~15秒ほどで25まで数えられるようになるそうです。そして、これだけで、観察力が高まるそうですよ。

シュルテ・テーブル

ソースコード

 ソースコードです。

from tkinter import Tk, Button, Canvas, Frame, Label, messagebox
from random import sample
from time import time

class SchulteTable(Frame):
    MARGIN = 10
    DISTANCE = 50
    WIDTH = 5
    HEIGHT = 5
    FONT_SIZE = 25
    text = list()
    def __init__(self, master=None):
        if master == None:
            master = Tk()
        super().__init__(master)
        master.title("Schulte Table")
        self.master = master
        self.pack()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

    def create_widgets(self):
        self.info = Label(self, text="Tap anywhere to start", font=("",14))
        self.info.pack(side="top")
        self.info.bind("<Button-1>", self.redraw_text)
        self.canvas = Canvas(self,width=self.MARGIN*2+self.WIDTH*self.DISTANCE, height=self.MARGIN*2+self.HEIGHT*self.DISTANCE)
        for i in range(self.WIDTH+1):
            self.line1 = self.canvas.create_line(self.MARGIN+i*self.DISTANCE,self.MARGIN,self.MARGIN+i*self.DISTANCE,self.MARGIN+self.DISTANCE*self.HEIGHT)
        for i in range(self.HEIGHT+1):
            self.line1 = self.canvas.create_line(self.MARGIN,self.MARGIN+i*self.DISTANCE,self.MARGIN+self.WIDTH*self.DISTANCE,self.MARGIN+i*self.DISTANCE)
        self.canvas.pack(side="top")
        self.canvas.bind("<Button-1>", self.redraw_text)

        self.quit = Button(self, text="QUIT", fg="red",
                              command=self.master.destroy)
        self.quit.pack(side="bottom")

    def redraw_text(self, event):
        if not len(self.text):
            self.info["text"] = "Tap anywhere to STOP"
            self.time = time()
            self.after(100, self.time_update)
        else:
            for t in self.text:
                self.canvas.delete(t)
            self.text = list()
            messagebox.showinfo("結果",f"かかった時間は、{self.elapse_time:0.2f}秒です。")
            self.info["text"] = "Tap anywhere to start"
            return
        counter = 0
        answer = sample(range(1,self.WIDTH*self.HEIGHT+1),self.WIDTH*self.HEIGHT)
        for x in range(self.WIDTH):
            for y in range(self.HEIGHT):
                self.text.append(self.canvas.create_text(self.MARGIN+x*self.DISTANCE+self.DISTANCE/2,self.MARGIN+y*self.DISTANCE+self.DISTANCE/2,text=str(answer[counter]),font=("",self.FONT_SIZE)))
                counter+=1

    def time_update(self):
        if not len(self.text):
            return
        self.elapse_time = time() - self.time
        self.info["text"] = f"Tap anywhere to STOP:{self.elapse_time:0.2f}"
        if len(self.text):
            self.after(10, self.time_update)

if __name__ == '__main__':
    shulteTable = SchulteTable()
    shulteTable.mainloop()

 このシュルテ・テーブル、ちょっと昔に作ったのですが、いけてませんね。なぜかFrameを継承しているし、そのせいでmasterをセットしなきゃいけなくなってるし、一行が異様に長いところがあるし、自分で作ったプログラムなのですが、、、ちょっとわかりずらいなぁ。

 とりあえず、動いているので、公開しようと思います。見れば見るほど修正したくなってきましたので、次回、リファクタリングします!

WordPress予約公開の仕方

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

 ブログの文章、何度も書いていると、少しずつですが、だんだんと文章を早くなってきた気がします。やっぱり継続は大事ですね。ずっと書くのが苦手だったのですよね。ずいぶん前にたまたま余裕があって一日に記事を二つ書いたことがあったのですけど、下書きのままにしておいて、翌日公開するというのもとてもおっくうなので、その時はすぐに公開しちゃいました。何日分も前もって書いておくのはぼくにはムリだなぁ、と、思っていましたのです。

 でもそれが、簡単に予約しておくことができることがわかりました。そんな機能があるとは夢にも思っていなかったので、昨日まで知らなかったのでした。

 そう、いつもは、記事を書き終わったら、編集画面の右上にあるボタンの「公開する」を押して、公開していました。そう、このボタンです。

「公開する」ボタン

 書いている途中で中断する時は、「下書きとして保存」 を、途中で仕上がりを確認するときには、「プレビュー」を押して確認する、それくらいしか使ってなかったのですよねぇ。

 とりあえず、「公開する」を押してみます。すると、次のように「公開してもよいですか?」と、聞いてきます。

「公開する」ボタンを押したところ

 で、いつもは、この「公開:今すぐ」のところをクリックして、カレンダーが出てくるので、時刻だけ00:00に修正してました。日にちは確認したことがなかったのでした。いつも過去の日付が出てくるので、このカレンダーは作ったタイミングで決まるのかなぁ、というくらいには思っていたのですけど。で、時刻を修正すると、過去の日付の00:00に設定されることになるので、今すぐに公開されるのですよね。当たり前ですよね。

いつも変更するのは時刻だけでした

 昨日は、なぜか日にちも変えてみよう、って、思って変更したのですよね。ちょっとやってみます。未来の日付、3月24日を選択しました。

「予約投稿」になった!

 すると、ごらんください!「公開」ボタンだったところが、なんと「予約投稿」に替わったのです。いや~、予約投稿なんてできたのですね!しかも、こんなに簡単に♪
 偶然とは言え、自分の知らない機能を発見して、ちょっとうれしい今日この頃です。この記事はこのままにして今日の夜に公開します!
 どんどん書いて、どんどん予約投稿、、、できるといいなぁ。(笑)

追記

 …と、いうことで、朝起きて確認してみましたが、なんと、まだ公開されていません。そういえば何となく気になっていました。日付と時刻、なんかずれてるような気がしてたのですよねぇ。試しに新規追加して作成された日時を確認してみます。

 おお、現在2020年3月24日の6時48分なのに、23日の12時48分の日時で作成されてしまいました。これは、、、完全にずれてますね。

 設定を確認してみたところ、ありました。「タイムゾーン」ですね。

タイムゾーンの設定

 同じタイムゾーンの都市またはUTC、、、ということで、ありました「アジア」から「東京」が選べるようです。選択して保存しました。再度、この文章を保存、あ、ボタンは「予約投稿」になってますが、、、これでどうでしょう。。。

予約投稿の失敗

 ははは、予約投稿の失敗だそうです。まあ、予約してた時間よりタイムゾーンが早くなりましたので、そりゃそうか、という感じで仕方ありませんね。でも、失敗もちゃんと考慮されてあって、WordPressってすごいですね!

 気を取り直してこの後の7時10分に予約投稿してみます。今度はどうかな。はい、今度はちゃんと予約済みになっていました!

 そして、無事、公開されてますね。ホッ。

Pythonプログラム 初期値の保存方法 Windowsスタイル

 今日も見に来てくださって、ありがとうございます。すっかり春めいてきましたね。

 Pythonでツールとかゲームを作りたいな、と、思ったときに必要になってくる設定ファイルって、どうしたらいいでしょう。ざっと考えただけでもいろんな選択肢があったのですが、ぼくの環境はWindows10ですので、Windowsスタイルの.iniファイルに保存して、読み書きするのがよさそうですね。ということで、標準モジュールのconfigparserを使ってみたいと思います。詳細はここに説明がありました。

読み込み方法

 まずは、試しにファイルを開いてみたいと思います。普段よく使っているサクラエディタのフォルダに.iniファイルがありましたので、そちらを見てみたいと思います。以下のスクリプトを実行してみます。

from configparser import ConfigParser

cfg = ConfigParser()
cfg.read("C:\Program Files (x86)\sakura\sakura.exe.ini")
for s in cfg.sections():
    print("["+s+"]")
    print(cfg.options(s))
    for o in cfg.options(s):
        print(o, cfg.get(s,o), cfg[s][o])

 Anaconda Promptからpythonコマンドを実行、その後スクリプトを実行しました。

実行結果

 1行目、configparseモジュールのConfigParserクラスをインポートしています。これで設定パーサーが使えるようになります。パーサーとは、構文解析を行うためのプログラムの総称です。今回のConfigParserは設定ファイルの構文を読み込んで利用できるようにしてくれます。

 3行目で、クラスのインスタンスを作成して、4行目で.iniファイルを読み込んでいます。

 セクションは、.iniファイルの中に[section]形式で指定された値です。5行目にあるように、cfg.sections()を実行することでセクション文字列のリストが取得できます。結果から、サクラエディタでは「Settings」というセクションが一つだけ設定されていることがわかります。

 セクションの中には、「=」で区切られたキーと値があります。6、7行目にあるように、cfg.options(s)とセクションを指定することでそのセクションのキー一覧が取得できます。注意点は、セクションは大文字小文字を区別するけど、オプションのキーは区別しない、ということでしょうか。

 値の取得方法は、いくつかありますが、簡単な方法として、二つ使ってみました。9行目の、cfg.get(s, o)とcfg[s][o]です。いずれもセクションとオプション(キー)を指定することで、値を取得できます。辞書のように使うことができるのでとっても便利ですね。

設定ファイルの作成、書き込み

 読み込みができたので、今度は書き込みです。test.iniファイルを新しく作成してみます。

cfg  = ConfigParser()
cfg.add_section("NewSection1")
cfg.set("NewSection1", "Option1", "Value1")
cfg.set("NewSection1", "Option2", "Value2")
cfg["OtherSection2"] = {"Option3":"Value3", "Option4":"Value4"}
with open("test.ini","w") as fp:
    cfg.write(fp)

 実行すると、test.iniファイルが出来上がりました。

[NewSection1]
option1 = Value1
option2 = Value2

[OtherSection2]
option3 = Value3
option4 = Value4

 サンプルとして二つの設定の仕方を試してみました。

1行目:まずはConfigParserのインスタンスを作成します。

2~4行目:add_sectionでセクションを追加して、追加したセクションにオプションをふたつセットしています。

5行目:辞書と同じ形式でセクションをセット、同時に別の辞書を使ってオプションを二つ追加しています。

6~7行目:最後にtest.iniファイルへ作成した値を保存しています。

おまけ

 これで、設定ファイルの読み込みと書き込みができるようになったのですが、これだと設定ファイル中のコメントが消えてしまうのですよね。まるっきり上書きですからね。ちょっと調べてみるとコメントを書くためのテクニックがありました。以下のスクリプトの通りです。

cfg = ConfigParser(allow_no_value=True)
cfg.add_section("SampleSection")
cfg.set("SampleSection","; これはセクションのコメントです。")
cfg.set("SampleSection","Option4", "Value4")

with open("test2.ini","w") as fp:
    fp.write("; これは全体のコメントのテストです。\n\n")
    cfg.write(fp)

 実行結果のtest2.iniファイルの内容です。

; これは全体のコメントのテストです。

[SampleSection]
; これはセクションのコメントです。
option4 = Value4

 ポイントは、1行目の「allow_no_value=True」をセットすることと、3行目のオプションの最初の文字を「;(セミコロン)」にしておく、ということですね。また全体のコメントを追加するためには、7行目のように、設定内容を書き込む直前にコメントを出力すればよいですね。

 他に、コメント文字は「#」、キーバリューの区切り文字に「:」も初期値として利用可能です。これらの値はConfigParserのインスタンスを作成するときに変更することができます。

まとめ

 設定ファイルの読み書きに、configparserが利用できます。コメント付きの設定ファイルの読み書きをしたいときには、ちょっとした工夫が必要です。

WordPressリンク切れの原因

 今日も見に来てくださって、ありがとうございます。桜の花が咲き始めましたね。

今日は、WordPressの話題です。WordPressを使い始めたころ、「石川さん、リンクが切れてますよ」と、リンク切れを教えてもらいました。フツーに文章を書いて、そのまま公開すると、そんなことが起きます。この文章もおそらくそうなると思いますので、ちょっと途中ですが、この状態で公開してみます。

 投稿の編集画面の右上の「公開する」ボタンを押すと、「公開しました。」というメッセージが出てきますので、「投稿を表示」をクリックします。

はい、でました。こんな感じです。

 サイトのトップページに行くと、記事はちゃんと見えているので、しばらく謎だったのですよね。いろいろと調べてみてわかった原因は、これでした。

URLをご覧ください。日本語になっています。

 ここのURLに日本語(マルチバイト文字)がセットされるとなぜか表示されない、ということがわかりました。どこでこれを修正するかというと、こちらです。(他に、投稿一覧から「クイック編集」を選択して「URLスラッグ」を修正することもできます。

文書-パーマリンクーURLスラッグ

ここを英数字と半角記号だけに修正すれば、正しくリンクをたどっていくことができるようになります。修正すると「投稿を表示」のURLが変更されて、正しく修正されたことを確認できます。では、修正してみましょう。

wordpressーcause-of-missing-linkに修正

 さて、どうでしょうか。うまくいくと思いますか?「更新」ボタンを押してから「投稿を表示」の部分をクリックしてみます。

またしても、失敗!今度は、なぜ?

 ぼくは、これでずいぶんと悩みました。と言っても30分くらいですけど。他のはこれでうまいこといったのに、どうしてできないのでしょうか。。。

 正解は、ハイフンが半角英数字のハイフンではなくて、マルチバイト文字になっていたからでした。ここの「wordpressーcause-of-missing-link」ひとつめのハイフンと、ふたつめからよっつめのハイフンとは、微妙に長さが違いますよね。わかりますか?

 そう、自分で入力したのですけど、日本語を削除してアルファベットを入力するときに、ひとつめだけハイフンの代わりに半角カタカナの長音の記号を入れてしまっていた、ということが原因でした。フォントが違えばすぐにわかるのでしょうけど、ブラウザだとほとんどわかりませんね。誰かのお役にたてるとうれしいです。

Pythonプログラム Oracleへの接続

 今日も見に来てくださって、ありがとうございます。今回は、pythonからOracleへ接続して、テーブルをつくったり、データをINSERT、SELECT、UPDATE、DELETEなどをやってみようと思います。

準備(まずはインストール)

 ぼくの現在の環境は、Windows10、Anaconda3(64bit)です。今回Oracleへ接続するためのモジュールに、cx_Oracleを使います。ちなみに、OracleはExpress Edition 18cです。Pythonでモジュールのインストールといえば、pipを使いますが、Anacondaの場合は、condaというコマンドを使うと整合性の取れたちょうど良いものを入れてくれるようなので、そちらを利用します。やり方がわからないときは、すぐにgoogle先生に聞いてみます。はい、インストールのやり方、ここにありましたね。ちなみに、cx_Oracleについては、丁寧なドキュメントがここにありました。(英語です。)

conda install -c anaconda cx_oracle

 コマンドが分かりましたので、手順を説明していきます。

まずは、Anacondaプロンプトの起動です。Windowsのメニューから以下の「Anaconda Prompt」を選択します。

Windows メニュー

すると、以下のようなプロンプト画面が表示されます。

Anaconda Prompt

 ここで、先ほどのコマンドを入力しましょう。おっと、condaの新しいバージョンが出ているといわれていました。

condaコマンドを走らせてアップデートしてください、ということなので、アップデートするのに、いったん中断しましょう。「Proceed ([y]/n)?」(続けますか?)と聞かれますのでnを入力、Enterキーを押して中断します。そして、今度は以下のcondaコマンドを更新するためのコマンドを入力して、結果を見ましょう。途中で「Proceed ([y]/n)?」(続けますか?)と聞かれますので、今度はyを入力してEnterを押して続けてください。

conda update -n base -c defaults conda

無事成功、ですね!
…と、思いましたが、正しくインストールできなかったようです。

失敗していました。

 ぱっと見た目はダウンロードして、解凍に成功していますので、バッチリできたねぇ、と、勘違いしても仕方ありませんよね。よく見ると「EnvironmentNotWritableError」という「環境が書き込みできないエラー」が発生しているようです。「The current user does not have write permissions to the target environment.」現在のユーザはターゲットの環境への書き込み権限を持っていません、ということですね。
 舞い戻って最初のコマンド入力のメッセージもよくよく眺めてみていたら、なんと、衝撃の事実が。あ、たいした事実ではありませんよ。(笑)

衝撃の事実

 そう、condaも一緒にアップデートされますよ、と記載があるじゃないですか。止めなくてもよかったのに。と、いうことで気を取り直して、書き込み権限のある状態でcondaを実行したいと思います。個別の権限設定があるかどうかは分かりませんが、こういう時はいつもAnaconda Promptを管理者権限で起動してからcondaコマンドを実行しています。

Anaconda Promptを管理者として実行

 Anaconda Promptを管理者として実行するために、Windowsメニューから「Anaconda Prompt」を右クリック、「その他」の「管理者として実行」を選択します。これで今度は成功するはず。ユーザーアカウント制御のダイアログボックスが表示された場合には、「このアプリがデバイスに変更を加えることを許可しますか?」の質問に対して「はい」を選択してください。これで今度は管理者としてAnaconda Promptが実行されました。

 左上に「管理者」と出力されていますね。
 では、気を取り直して再び先ほどのコマンドを実行します。

conda install -c anaconda cx_oracle

 今度こそ成功だね、と、思いましたが、なんと、またしてもエラーが発生。

またしてもエラー発生

 アクセスが拒否されました、ということですが、、、管理者権限で実行したのにねぇ。Qtのパッケージにアクセスするのに失敗しているようですね。ええと、もしかしたら、開発環境(IDE)のSpyderを使っているのが原因でしょうか。ぜんぜん気にしていませんでしたが、Spyderが起動中でした。Spyderも確かQtを利用していたと思いますので、おそらくこいつが掴んでいるためにアクセスが拒否されたのでしょうね。と、いうことでSpyderを終了してもう一度実行してみます。

今度はやっと成功しました。

 若干先ほどのパッケージと内容が変わっているのが気になりますが、なんとか成功したようです。

接続確認

 では、さっそく接続確認してみます。接続確認のスクリプトは以下の通りです。

import cx_Oracle

username = "ishikawa" # ユーザー名は適宜変更してください。
password = "********" # パスワードも適宜変更してください。
conn = cx_Oracle.connect(username, password, "127.0.0.1:1521/xepdb1")
print(conn.version)

 最初に必要なのは、import cx_Oracleとcx_Oracleモジュールをインポートすることです。接続には、モジュールで定義されているconnectメソッドを使います。接続後、versionが出力できれば、接続ができた、ということが確認できるでしょう。

接続成功!

はい、接続に成功したようです!

テーブル作成

 では、続けてテーブルを作成します。コネクションからcursor()を呼び出してカーソルを作成して、カーソルからexecute()を使ってSQLを実行します。

cur = conn.cursor()
cur.execute("create table poi( n number, v varchar2(20), c char(10), d date )")
テーブル作成成功!

 はい、成功したようです。念のため、SQL*Plusで確認してみました。あ、ちなみにSQL*Plusとは、Oracleのコマンドラインツールで、SQLを発行したり、結果をファイルへ出力したりできる、基本的なツールです。Oracleをインストールした環境にはたいていインストールされていますので、いろいろな現場へ行く人は使い方に習熟しておくとよいと思います。

テーブル作成結果をSQL*Plusで確認

 ちゃんと作成されてましたね。

データのINSERT、SELECT、UPDATE、DELETE

 次に、SQLのDMLを確認していきたいと思います。スクリプトは以下の通りです。

cur.execute("insert into poi values ( 1, 'abc', 'def', sysdate )")
for row in cur.execute("select * from poi"):
    print(row)

cur.execute("update poi set n = 2 where n = 1")
conn.commit()
cur.execute("delete from poi where n = 2")
conn.commit()

 まずINSERTを実行して、内容をSELECTしてみます。そして、UPDATEしてから内容をコミットします。

INSERT、SELECT、UPDATE、COMMITの実行結果

 ごらんの通り、SELECTの結果はタプルのシーケンスとして戻されるのですね。日付はdatetimeモジュールのdatetimeで戻されるのですね。なるほど。
 ちゃんと更新されているかどうか、SQL*Plusからも確認してみます。

INSERT、UPDATE、COMMITの結果をSQL*Plusから確認

 ちゃんとNが2に更新されていますね。続いてDELETEを実行して、コミットします。

DELETEとCOMMITの実行

再度、SQL*Plusから確認してみます。

DELETEとCOMMITの実行結果をSQL*Plusから確認

 はい、ちゃんと削除されていました。

まとめ

 今回、cx_Oracleを使ってオラクルへ接続して、簡単なSQLを発行してみました。とりあえずはこれだけできるようになっていれば、簡単なアプリケーションは実現できそうですね。でも、エラー発生時のハンドリングとか、ストアドプログラムの呼び出しとか、変数をバインドしたりとか、ちょっと考えただけでもまだまだやらないといけないこと、たくさんありますね。また機会があれば、書いていきたいと思います。

 今回の注意点としては、インストールはcondaコマンドを管理者権限で実行する。このとき余計なプログラムは終了させておく。インストールのメッセージが英語だからと出力を適当に流さない、ということぐらいでしょうかねぇ。

PythonでGUIプログラミング キー入力を受け付ける

 今日も見に来て下さって、ありがとうございます。地道ながらに更新を続けたせいか、コロナウィルスで引きこもり中の余裕のある人たちのおかげか、週間のユーザ数が三桁に達するようになってまいりました。以前書いた「Pythonでプログラミング キー入力を受け付ける」のアクセス数がなぜか多いので、調子にのってGUI版を書いてみることにしました。

出来上がりイメージ

 出来上がりイメージです。ウィンドウの中にキャンバスをつくって、その中に黄色い丸と青い丸を書きました。マウスを動かすと、丸がついて回ります。そして、矢印キーを押すと、キーを押した方向へ丸が移動します。という、単純なものです。

出来上がりソースコード

とりあえず、動かしてみたいんじゃ、という忙しい方のために、まずはソースを貼っておきます。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Moving in Canvas")
        self.pos = (0,0)
        self.pressed = {}
        self.canvas = tk.Canvas(width=600, height=400, background="white")
        self.canvas.pack()
        self.item = self.canvas.create_oval(10, 10, 40, 40, fill="yellow", tag="t")
        self.inner_item = self.canvas.create_oval(20, 20, 30, 30, fill="blue", tag="t")
        self.canvas.bind("<Motion>",self.move_by_mouse)
        self.bind("<KeyPress>",self.key_pressed)
        self.bind("<KeyRelease>",self.key_released)
        self.move_by_key()

    def move_by_mouse(self, event):
        if self.pos == (0,0):
            x0, y0, x1, y1 = self.canvas.coords(self.item)
            px = x0 + (x1 - x0) // 2
            py = y0 + (y1 - y0) // 2
            dx = event.x - px
            dy = event.y - py
            self.canvas.move("t", dx, dy)
            self.pos = (event.x, event.y)
            return
        dx = event.x - self.pos[0]
        dy = event.y - self.pos[1]
        self.pos = (event.x, event.y)
        self.canvas.move("t", dx, dy)
    
    def key_pressed(self, event):
        self.pressed[event.keysym] = True
        self.pos = (0,0)
    
    def key_released(self, event):
        self.pressed.pop(event.keysym, None)
    
    def move_by_key(self):
        dx, dy = 0, 0
        m = 5
        if "Up" in self.pressed:
            dy -= m
        if "Down" in self.pressed:
            dy += m
        if "Left" in self.pressed:
            dx -= m
        if "Right" in self.pressed:
            dx += m
        x0, y0, x1, y1 = self.canvas.coords(self.item)
        px = x0 + (x1 - x0) // 2 + dx
        py = y0 + (y1 - y0) // 2 + dy
        if 0 <= px <= self.canvas.winfo_width() and 0 <= py <= self.canvas.winfo_height():
            self.canvas.move("t", dx, dy)
        
        self.after(10, self.move_by_key)

if __name__ == '__main__':
    app = App()
    app.mainloop()

内容説明

 まずは、1行目、「import tkinter as tk」で、tkinterモジュールをインポートします。「as tk」と指定してあるのは、グローバルな名前空間が汚されないようにするためです。チュートリアルでありがちな、「from tkinter import *」はダメな例なのでまねしないようにしましょう。

 次に、3行目、「class App(tk.Tk):」GUIアプリケーションのトップレベルは、必ずtk.Tkになりますので、これを継承してアプリケーションを作成します。

 4、5行目「def __init__(self):」では、アプリケーションの初期化を行います。まずは、親のメソッドを呼び出すため、「super().__init__()」をコールしています。ここまでは、もうお約束ですので完全に記憶しましょう。

 6行目「self.title("Moving in Canvas")」ウィンドウのタイトルをセットしています。tkinterでは特に何も意識せず日本語も使えます。

 7、8行目は、あとで使う変数を初期化しています。

 9~12行目で、Canvasをつくって、その上に丸を書いています。ポイントは、この丸を書くときに「tag="t"」とタグをセットしているところでしょうか。あとで出てきますが、キャンバス上のモノは、IDかタグで動かしたり属性を変更したりすることができます。IDだと一つしか動かせませんので、今回は二つの丸を動かすために、タグを指定しました。

 13~15行目は、イベントにバインドしています。どういうことかというと、例えばこの「self.canvas.bind("<Motion>",self.move_by_mouse)」の場合だと、self.canvas<Motion>(マウスポインタがキャンバスで動いた!)というイベントが発生したときには「self.move_by_mouse」を呼び出してね、と、割り当てている、ということになります。日本語のニュアンスだと結び付けている、というのが適当でしょうか。
 ここでは、3つのイベントをそれぞれ割り当てています。

  • <Motion> マウスが動いた → self.move_by_mouse
  • <KeyPress> キーが押された → self.key_pressed
  • <KeyRelease> キーが離された → self.key_released

 マウスによる移動と、キー入力による移動はそれぞれ独立しています。まずはマウスによる移動の方から説明します。

マウスによる移動

 マウスが動くたびに、<Motion>イベントが発生します。14行目でバインドしたので、マウスの動きに合わせてself.move_by_mouseが呼び出されます。マウスに合わせて動作させるのは、これだけで充分です。ポイントは、self.canvas.moveで指定できるのは、指定したタグを移動するオフセット値になることです。現在位置からx軸に+10、y軸にー20といった風に設定することになります。eventで取得できるのが左上のコーナーを0,0との基準にしてプラスに増えていく座標になっています。このため、最初の動き出しの時だけは、動かずに動作開始点を保持するだけにして、次のイベントが発生したときに、前の位置から5,3動く、といった指定になるようにしました。

 ちなみに、x0, y0, x1, y1 = self.canvas.coords(self.item)は、self.itemの左上の原点からの座標を取得するメソッドです。self.itemを矩形で切り取って左上の座標と右下の座標を同時に取得しています。self.itemの中心座標を計算するのに、px = x0 + (x1 - x0) // 2py = y0 + (y1 - y0) // 2としています。その後、dx = event.x - pxdy = event.y - pyにて、中心点からマウスカーソルの座標までのそれぞれの移動距離を計算しています。その後、self.canvas.move("t", dx, dy)として、マウスカーソルまでself.itemを移動します。

キーによる移動

 キーが押されるたびに、<KeyPress>イベントが発生します。押したキーを話すたびに、<KeyRelease>イベントが発生します。イベントにはそれぞれ、key_pressedkey_releasedがバインドされていました。このため、キーが押されると、8行目で初期化されたディクショナリ「self.pressed = {}」の中に、押されたキーのシンボルがTrueとして登録されます。例えば、右キーを押すと、ディクショナリの中身は{'Right': True}という風になります。キーは同時に押すこともできますので、例えば上と右キーを同時に入力するとディクショナリの中身は{'Up': True, 'Right': True}のようになります。ソースコードを見ればわかると思いますが、キーのシンボルはevent.keysymで取得しています。

 初期化の説明の時にはさらりと飛ばしましたが、初期化(__init__(self))の最後の行、16行目で、self.move_by_key()を呼び出しています。このmove_by_keyは呼び出されると、最後にself.after(10, self.move_by_key)を呼び出すことで、自分自身を10ミリ秒後に呼び出すことで、無限ループを開始します。このループにて、キー入力を処理しています。

 具体的には、キー入力で上下左右の移動距離(ここでは5)をセットして、キャンバスのmoveメソッドを呼び出すself.canvas.move("t", dx, dy)ことでアイテムを動かしています。その直前のif文は、画面の外へはみ出して移動しないように制御しています。

まとめ

 上記のキー入力制御のアプローチは、個別にイベントをバインドするやりかたよりも好ましいと思います。個別にバインドした場合は、キー入力しない限りイベントが発生しないので、位置を飛ばして移動するような移動には使えますが、よりスムーズな移動には今回のようなイベントループで制御する必要があります。

 これでtkinterを使うときにキー入力やマウスによるイベントの制御はバッチリですね!

PowerBuilderでダウンロードフォルダを取得

今日も見に来てくださってありがとうございます。今日も楽しく更新します♪

珍しく、PowerBuilderの記事を書いてみることにしました。今のおしごとでは、PowerBuilder8.0.3というかなーり古い開発ツールを使っていますので、ニーズはほとんどないと思いますけど、自分用メモ、といういことでご容赦ください。

今回、Webからダウンロードしたファイルを読み込んで処理をする必要があって、ファイルの初期値をどうしようかなぁ、と、思ったのが発端です。とりあえず、ダウンロードフォルダにファイルが入るだろうから、そのフォルダを示しておけば大丈夫でしょう、ということでフォルダ名の取得方法を調べました。

こちらに紹介がありましたが、とうぜんPowerBuilderでの使い方ではありませんね。VBScriptでは、以下のように入力すればよいということでした。

MsgBox CreateObject("Shell.Application").Namespace("shell:Downloads").Self.Path

これを手掛かりに作ったのが以下のスクリプトです。

OLEObject lole_shell
lole_shell = CREATE OLEObject
lole_shell.ConnectToNewObject('Shell.Application')
OLEObject lpo
lpo = lole_shell.namespace("shell:Downloads")
if not isNull(lpo) then
    string ls_path
    ls_path = lpo.Self.path
    if directoryExists(ls_path) then
        sle_nm_file.text = ls_path + "\"
    else
        sle_nm_file.text = ""
    end if
else
    sle_nm_file.text = ""
end if
DESTROY lole_shell

ポイントは、lole_shell.namespace("shell:Downloads")を取得して、値がセットされたかどうかをチェックしているところです。ぼくの開発環境は古くて値がセットされず、エラーになっちゃうのですよね。でも、作ったプログラムをWin10環境で動かせば、値が取得できる、というトリックです。とりあえずユーザーが利用できればいいのですけど、ぼくが開発環境でその画面を開いたら異常終了、というのはいただけませんからねぇ。

Tkinterのレイアウト方法は3種類

 今日も見に来てくださってありがとうございます。着々と読者が増えているようでうれしいです。がんばって書いていきます。

 ここのところtkinterで遊んでいます。tkinterではウィジェットと呼ばれる部品が用意されているのですが、それをウィンドウ内の適切な位置に配置する必要があります。tkinterではその配置する方法が、pack、grid、placeの3種類あります。全部使ってみました、という例を作ってみました。ちなみにこれらは、ジオメトリマネージャ(geometry manager)と呼ばれています。配置を管理するための仕組みですね。これらが組み合わせられるか、お試しでプログラムを作ってみました。

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

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("geometry manager test")

        gridframe = tk.LabelFrame(self, text="グリッドのフレーム")
        gridframe.pack()
        g = GridFrame(gridframe)
        g.pack()
        
        packframe = tk.LabelFrame(self, text="パックのフレーム")
        packframe.pack()
        pa = PackFrame(packframe)
        pa.pack()

        placeframe = tk.LabelFrame(self, text="プレースのフレーム")
        placeframe.pack(ipadx=10, ipady=5, expand=True, fill=tk.BOTH)
        pl = PlaceFrame(placeframe)
        pl.pack()

        label = tk.Label(text="ラベル")
        button = tk.Button(text="ボタン",command=self.destroy)
        label.pack()
        button.pack()

class GridFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        label = tk.Label(self,text="gridのラベル")
        label.grid(column=0,row=0,padx=10,pady=10)
        button = tk.Button(self,text="gridのボタン")
        button.grid(column=1,row=1,padx=10,pady=10)

class PackFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        label = tk.Label(self, text="packのラベル")
        button = tk.Button(self, text="packのボタン")
        label.pack(padx=20,pady=20)
        button.pack(padx=20,pady=20)
        
class PlaceFrame(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master,width=300,height=50)
        label = tk.Label(self, text="placeのラベル")
        button = tk.Button(self, text="placeのボタン")
        label.place(relwidth=0.25, relheight=0.25)
        button.place(anchor=tk.N, x=200, y=20, width=80, height=30)

if __name__ == "__main__":
    app = App()
    app.mainloop()

gridは格子状に分けた場所に部品を配置するイメージです。行と列で位置を指定することができます。また、columnspanrowspanを指定することで幅や高さを増やすことができます。packは真空パックのイメージでしょうか。部品をつくって、詰める感じです。placeは位置や大きさを直接指定するイメージですね。それぞれの詳細はまたいずれどこかでまとめようと思います。

ひとつのコンテナの中で異なる方法を混ぜて使うことはできません。エラーが発生して処理が中断してしまいます。今回のこの例のようにFrameを継承したコンテナの中に部品を配置して、それらを組み合わせる、というのがコツのようです。こうすることで、それぞれが独立して影響を及ぼさないようにつくることができます。

たったこれだけの部品を並べるのに、こんなに書かないといけないのは、けっこう面倒ですよね。でもまあ、つくって、並べて、つくって、並べて、と、これより短くするのはかなり難しい(ムリ)でしょうね。

Tkinter情報を取得するダイアログ

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

先日からの続きですが、tkinterに用意されているダイアログで、情報取得用のダイアログをまとめてみました。

実行結果
askopenfilename
asksaveasfilename
askopenfilenames
askdirectory
askcolor
askfloat
askinteger
askstring

画像を張り付けるとそれだけで記事が多くなるなぁ、というのは置いといて、上記の画面を実行するためのスクリプトは以下の通りです。

import tkinter.filedialog as fd
import tkinter.colorchooser as cc
import tkinter.simpledialog as sd

class OtherDialogSample(tk.Tk):
    
    def create_dialog_button(self, group, dialog, argcount=None):
        def command():
            if argcount == 2:
                ret = dialog(title=dialog.__name__, prompt=dialog.__doc__)
            else:
                ret = dialog()
            print(ret)
        button = tk.Button(group, text=dialog.__name__, command=command)
        button.pack(padx=10,pady=10,fill=tk.BOTH)

    def __init__(self):
        super().__init__()
        self.title("Other Dialog Sample")
        self.g1 = tk.LabelFrame(self, text="ファイルダイアログのサンプル")
        self.g1.pack(padx=10, pady=10,side=tk.LEFT)
        self.create_dialog_button(self.g1, fd.askopenfilename)
        self.create_dialog_button(self.g1, fd.asksaveasfilename)
        self.create_dialog_button(self.g1, fd.askopenfilenames)
        self.create_dialog_button(self.g1, fd.askdirectory)
        self.g2 = tk.LabelFrame(self, text="その他のダイアログのサンプル")
        self.g2.pack(padx=10,pady=10,fill=tk.BOTH,side=tk.LEFT)
        self.create_dialog_button(self.g2, cc.askcolor)
        self.create_dialog_button(self.g2, sd.askfloat, 2)
        self.create_dialog_button(self.g2, sd.askinteger, 2)
        self.create_dialog_button(self.g2, sd.askstring, 2)

if __name__ == "__main__":
    ods = OtherDialogSample()
    ods.mainloop()

いろいろと説明したいことはありますが、、、とりあえずまとめます。

ダイアログタイトル戻り値キャンセル時備考
askopenfilename開くフルパス文字列空文字列指定ファイルが存在しない場合メッセージ出力、続行不可
asksaveasfilename名前を付けて保存フルパス文字列空文字列既存ファイル選択時は警告メッセージ出力
askopenfilenames開くフルパス文字列のタプル空文字列複数ファイル選択可能
askdirectoryフォルダの選択フォルダ文字列空文字列
askcolor色の設定タプル((R,G,B),色表現文字列)(None,None)
askfloat指定文字列(必須)float値Nonefloat値以外はエラー
askinteger指定文字列(必須)int値Noneint値以外はエラー
askstring指定文字列(必須)string値None

気になったのはファイルダイアログ関連の戻り値で、なぜか区切り文字がUnix風の「/(スラッシュ)」でした。ぼくの環境はWindowsなので、「\(円マーク)」を期待していたのですけど違っていました。これはおそらくTkがもともとUnix環境用として開発され始めたことが原因なんじゃないかなぁ。

askcolorの戻り値も気になりました。R,G,Bがなぜかfloat型なのですよねぇ。Tkのページでは0~65535のRed、Green、Blueの値を返す、となっているのですけど、tkinter内では受け取った値を256で割り算しているのです。取得した値を調べてみると2バイト値には同じ値が入力されていました。(例:0xFFFF、0xC0C0など)16ビットのうち、実質8ビットだけ必要なので、256で割り算して算出していたのでしょうね。これは、Python2系の処理が/で割り算した答えをint型に返していた名残なのだと思います。Python3系では、//で割り算すればint型にしてくれるのですけど、過去の経緯もあって変えられないのかな。

その他の特記事項としては、ファイルダイアログではパラメータのinitialdirで初期ディレクトリを指定可能で、今回選択したディレクトリが次回のinitialdirにセットされます。filetypeで表示するファイルタイプを指定可能です。ファイルタイプはfiletype=(("すべてのファイル","*.*"), ("テキストファイル", "*.txt *.log *.csv"))のように、表示するラベルとファイル名のマッチングパターンの二つの文字列を含むタプルのタプルで指定します。拡張子が未指定の時のために、defaultextensionで拡張子の初期値をセットすることも可能です。
simpledialogで定義されているaskfloataskintegeraskstringについては、初期値と最小値、最大値がそれぞれ、initialvalueminvaluemaxvalueで指定可能です。

まずは、用意されているダイアログをうまく使えるようになりたいですね。