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で指定可能です。

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

Tkinterダイアログいろいろ

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

今回は、tkinterで用意されているダイアログを見てみましょう。英語でダイアログとは、会話のことで、コンピューター用語としては、メッセージを出力するポップアップ画面のことを言います。ここでは、情報を出力して、ユーザーにアクションを促すダイアログを紹介します。ボタンがひとつのダイアログは3種類、ボタンがふたつのダイアログは4種類、ボタンが三つのタイプはひとつ用意されています。ダイアログを表示するためのメニュー画面を作ってみました。こんな感じです。

実行結果

それぞれボタンを押すと以下のダイアログが開きます。ダイアログのボタンを押すと、コンソールに戻り値を出力します。

showinfo
showwarning
showerror
askokcancel
askquestion
askretrycancel
askyesno
askyesnocancel

上記の画面を出力するスクリプトは、以下の通りです。

import tkinter as tk
import tkinter.messagebox as mb

class DialogSample(tk.Tk):

    def create_button(self, group, dialog):
        def command():
            ret = dialog(master=self, title=dialog.__name__, message=dialog.__doc__)
            print(ret)
        b = tk.Button(group, text=dialog.__name__, command=command)
        b.pack(padx=20, pady=10, fill=tk.BOTH)

    def __init__(self):
        super().__init__()
        self.title("Dialog Sample")
        self.g1 = tk.LabelFrame(self, text="ボタンひとつのダイアログ")
        self.g1.pack(padx=10, pady=10,side=tk.LEFT, fill=tk.BOTH)
        self.create_button(self.g1, mb.showinfo)
        self.create_button(self.g1, mb.showwarning)
        self.create_button(self.g1, mb.showerror)
        self.g2 = tk.LabelFrame(self, text="ボタンふたつのダイアログ")
        self.g2.pack(padx=10, pady=10, side=tk.LEFT, fill=tk.BOTH)
        self.create_button(self.g2, mb.askokcancel)
        self.create_button(self.g2, mb.askquestion)
        self.create_button(self.g2, mb.askretrycancel)
        self.create_button(self.g2, mb.askyesno)
        self.g3 = tk.LabelFrame(self, text="ボタンみっつのダイアログ")
        self.g3.pack(padx=10, pady=10, side=tk.RIGHT, fill=tk.BOTH)
        self.create_button(self.g3, mb.askyesnocancel)
            
if __name__ == "__main__":
    d = DialogSample()
    d.mainloop()

ダイアログを実行すると値が帰ってきます。ボタンがひとつのダイアログは、okの文字列が、ボタンがふたつのダイアログはaskquestionyes/noで、それ以外はTrue/Falseが戻されました。ボタンがみっつのダイアログはなんと、それぞれのボタンでTrue/False/Noneを戻してきました。まとめると以下の表のようになります。

ダイアログボタン1ボタン2ボタン3戻り値1戻り値2戻り値3×ボタン
showinfoOKokok
showwarningOKokok
showerrorOKokok
askokcancelOKキャンセルTrueFalseFalse
askquestionはいいいえyesno使用不可
askretrycancel再試行(R)キャンセルTrueFalseFalse
askyesnoはい(Y)いいえ(N)TrueFalse使用不可
askyesnocancelはい(Y)いいえ(N)キャンセルTrueFalseNoneNone

面白いなぁ、と、思ったのは、戻り値の違いです。TrueFalseNoneはそうかな、という気持ちになるのですけど、yesnookは微妙な気持になります。ま、okはチェックしないだろうから問題ないですけど、yesnoは、どちらか絶対答えてほしいという気持ちの表れなのかな。そして、askquestionaskyesnoは絶対混乱すると思いますよね。(笑)

【追記】

tkinterのmessagebox.pyのソースコードを見ていたら、ABORTRETRYIGNOREという未使用の変数が定義されていました。本家のTkで使えるから定義だけはしておいたのでしょうか。messagebox.pyでダイアログまで作っておいてくれればいいのにね。と、いうことで作ってみました。

abortretryignore

戻り値は、それぞれabortretryignoreとなっていました。
tkinterでダイアログは未定義なので、一覧を修正するのはどうかなぁ、と思ったので、どうやって作ったのか、ソースだけ載せておきます。

    def create_abortretryignore_button(self, group):
        def command():
            ret = mb._show(master=self,
                           title="おまけ(名無しダイアログ)",
                           message="このダイアログはtkinterでは定義されていません。",
                           icon=mb.QUESTION,
                           type=mb.ABORTRETRYIGNORE)
            print(ret)
        b = tk.Button(group, text=mb.ABORTRETRYIGNORE, command=command)
        b.pack(padx=20, pady=10, fill=tk.BOTH) 

Tkinterカーソルの種類(マウスポインターの種類)

 今日も見に来てくださって、ありがとうございます。最近コロナウイルスの話題で世間は騒がしいですが、皆さんはどうでしょうか。

 ここのところ、tkinterにハマっています。今日は、tkinterで用意されているカーソルにどんなものがあるのか、ということで調べてみました。ご存知の通りTkで定義されているものが利用されます。本家のホームーページのここに利用可能なカーソルの一覧がありました。どんなカーソルがあるか、画像を取れればよかったのですが、実際に動かせるソースの方がいいかも、ということで、残しておきます。実行すると、以下のような画面が開きます。白いラベルをポイントすると、カーソルが変わります。

実行結果画面
ポイントしたところ(カーソルがhand2に変わっている)

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

import tkinter as tk

class CursorTest(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Curosor demo")
        self.group1 = tk.LabelFrame(self, padx=15, pady=10, text="ラベルを選択してください。カーソルが変更されます。")
        self.group1.pack(padx=10, pady=5)
        self.windows_native = ["X_cursor","arrow","based_arrow_down","based_arrow_up",
                               "boat","bogosity","bottom_left_corner","bottom_right_corner",
                               "bottom_side","bottom_tee","box_spiral","center_ptr",
                               "circle","clock","coffee_mug","cross",
                               "cross_reverse","crosshair","diamond_cross","dot",
                               "dotbox","double_arrow","draft_large","draft_small",
                               "draped_box","exchange","fleur","gobbler",
                               "gumby","hand1","hand2","heart",
                               "ibeam","icon","iron_cross","left_ptr",
                               "left_side","left_tee","leftbutton","ll_angle",
                               "lr_angle","man","middlebutton","mouse",
                               "none","pencil","pirate","plus",
                               "question_arrow","right_ptr","right_side","right_tee",
                               "rightbutton","rtl_logo","sailboat","sb_down_arrow",
                               "sb_h_double_arrow","sb_left_arrow","sb_right_arrow","sb_up_arrow",
                               "sb_v_double_arrow","shuttle","sizing","spider",
                               "spraycan","star","target","tcross",
                               "top_left_arrow","top_left_corner","top_right_corner","top_side",
                               "top_tee","trek","ul_angle","umbrella",
                               "ur_angle","watch","xterm",]
        for i, c in enumerate(self.windows_native):
            l = tk.Label(self.group1, text=c, cursor=c, bg="white", font=(22))
            l.grid(row=i//4, column=i%4, padx=3, pady=3, sticky=tk.W+tk.E)

if __name__ == "__main__":
    cursorTest = CursorTest()
    cursorTest.mainloop()

 ぼくの使っている環境はWindow10のAnaconda3で、それで動作確認しました。ちょっと美しくないカーソルもあるので、使えるものは限定されそうですね。

 ちなみに、カーソルを指定しているのは、forループの中にある、Labelを作成するところのcursor=cという指定のみです。Tkのドキュメントにもあるように、ざっと見たところ、すべてのウィジェットでこの指定でポイントしたときのカーソルを指定できるようです。