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のドキュメントにもあるように、ざっと見たところ、すべてのウィジェットでこの指定でポイントしたときのカーソルを指定できるようです。

Anaconda3 Python実行時にエラーが出始めました。

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

Anaconda3ディストリビューションのpythonを使っていると、なぜか起動時にエラーが出るようになってしまいました。

(base) C:\work>python
Python 3.7.3 (default, Mar 27 2019, 17:13:21) [MSC v.1915 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
Failed calling sys.__interactivehook__
Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\site.py", line 439, in register_readline
    readline.read_history_file(history)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pyreadline\rlmain.py", line 165, in read_history_file
    self.mode._history.read_history_file(filename)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pyreadline\lineeditor\history.py", line 82, in read_history_file
    for line in open(filename, 'r'):
UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 1549: illegal multibyte sequence
>>> 

エラーはUnicodeDecodeErrorということで、'cp932'(Windowsのデフォルトで概ねShift JISのコードのことです)に変換しようとがんばってみたんだけど、できなかったよ~、ごめんね、という意味です。何にもしていないはずの起動時にどうして出るのでしょうね。エラーメッセージに「history.py」とありますので、きっとインタプリタの履歴の中に変換できない文字コードが使われているということでしょう。エラーになっても実行できますが、履歴は保存されないので不便です。ちなみに、履歴は「C:\Users\username\.python_history」というファイルの中に格納されていました。中身を確認すると、ありました「髮サ隧ア逡ェ蜿キ = 10」というよくわからない行が。UTF-8→SJISに変換してみると「電話番号 = 10」という行になりました。確かに、なんかそんなことを実行した気がする。電話番号(笑)を保存してpythonを実行し直したところ、スムーズに開始しました。ただ、再度終了して起動したところ同様のエラーが出始めます。日本語の行を履歴ファイルからすべて削除すれば出なくなりますが、もっと根本的に解決したいですね。

そこで、histroy.pyファイルをよく読んでみました。どうやら、保存するときに文字列はちゃんとUnicodeになるようにしておいて、バイナリファイルとして保存するようにしているけど、開くときはデフォルトのテキストファイルとして読み込んでおり、デフォルトのエンコーディングが'cp932'になっているのが原因のようです。そこで、このファイルの中のread_history_file関数のファイルを開くところにencodingにutf8を指定するように変更してみます。そう、ちょうどエラーが発生しているhistory.pyファイルの82行目です。

【修正前】
            for line in open(filename, 'r'):
【修正後】
            for line in open(filename, 'r', encoding='utf8'):

はい、うまいこと動くようになりました。これで解決ですね!
ちなみにぼくの環境ではこのhistory.pyファイルは書き込み不可になっているものもありました。その場合は、ファイルのプロパティからセキュリティタブを選んでアクセス許可を編集して書き込みを許可すれば編集できます。

でも、なんでテキストで書いて、バイナリで読んでるのかなぁ。せっかくUnicodeを保証してるのに、最初からUnicodeで読んでほしいですよね。ソースファイル調べてみると、GitHubに登録されていました。issueにも同じエラーについて登録されていました。最近gitを使い始めたので、ちょっと修正依頼をお願いしてみましょう。フォークして、編集をして、プルリクエスト、終了。これであってるかな。ま、たぶん大丈夫でしょう。みんな初心者には優しいはずだし。リクエストが受け付けられるのを待ちましょう。あ、、、別のissueでもnone-latin charで同じようなエラーが出ていて、そちらも同様の修正でプルリクエストがでてる、というのを見つけちゃいました。結構プルリクエストは放置されているようですねぇ。。。ま、いっか。

Pythonで16進数ダンプするプログラムをつくる その2

今日も見に来てくださって、ありがとうございます。なぜか着々と読者数が増えています。

先日、Pythonで16進数ダンプするプログラムをつくる、ということで記事をアップしました。でも、しばらくして、そういえば、あれはスクリプトをつくっただけで、プログラムとは言えないよねぇ、という気持ちになってまいりました。再利用もできないし、拡張性にも乏しいですからね。と、いうことで今回は、先日のスクリプトをモジュールにしてみる、ということをテーマにしようと思います。

Pythonでモジュールをつくると、import文で読み込んで利用することができるようになります。先日は、「python hexdump.py ファイル名」で実行するようにしましたが、この機能を残したまま、プログラム中で「h=hexdump(filename)」のように文字列を返すようにしてみたいと思います。

今回も、時間のない方に、完成したソースは以下の通りです。

from os.path import exists

def hexdump(filename, separator=" ", enterper=20, separateper=10, start=None, end=None):
    '''処理:ファイルの内容を16進数でダンプする
引数:separator 区切り文字 初期値空白 バイトごとに挿入する
   enterper 何バイトごとに改行するか
   separateper 何バイトごとに追加で区切り文字を入れるか
   start 何バイト目からスライスの開始値
   end 何バイト目までスライスの終了値
    '''
    if not exists(filename):
        raise Exception(f"指定されたファイルは存在しません。[{filename}]")

    with open(filename, 'rb') as f:
        data = f.read()

    output = ""
    data = data[start:end]
    for i, x in enumerate(data, 1):
        output += f'{x:02x}' + separator
        if enterper > 0 and i % enterper == 0:
            output += "\n"
        elif separateper > 0 and i % separateper == 0:
            output += separator

    return output

if __name__ == '__main__':
    from sys import argv
    howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'
    if len(argv) != 2:
        print(howtouse)
        exit()
    print(hexdump(argv[1]))

取り急ぎ、まずは、関数を作ってみることにしましょう。関数は、defを使って定義します。前回のスクリプトは以下のように変更されます。

from os.path import exists

def hexdump(filename):
    '''処理:ファイルの内容を16進数でダンプする'''
    if not exists(filename):
        raise Exception(f"指定されたファイルは存在しません。[{filename}]")

    with open(filename, 'rb') as f:
        data = f.read()

    output = ""
    for i, x in enumerate(data, 1):
        output += f'{x:02x} '
        if i % 20 == 0:
            output += "\n"
        elif i % 10 == 0:
            output += ' '

    return output

if __name__ == '__main__':
    from sys import argv
    howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'
    if len(argv) != 2:
        print(howtouse)
        exit()
    print(hexdump(argv[1]))

上から順番に見ていきましょう。まずdefを使って関数を定義していますが、その次の行です。'''で始まって、'''で終了していますが、これは三連引用符といいまして、間の改行を許して文字列を生成してくれます。このdefの直後の文字列はドキュメンテーション文字列として扱われ、大規模開発など、たくさんの人と協業してプログラムをつくるときにとても役立つ説明になります。モジュールをインポートすると、以下のようにhelp関数を使って確認することができます。

>>> from hexdump import hexdump
>>> help(hexdump)
Help on function hexdump in module hexdump:

hexdump(filename)
    処理:ファイルの内容を16進数でダンプする

>>>                                 

このドキュメンテーション文字列、実際には、関数オブジェクトの属性__doc__に格納されています。help関数はこの属性値を整形して出力しているのでしょうね。

>>> hexdump.__doc__
'処理:ファイルの内容を16進数でダンプする'
>>>                                             

さて、次にさらりと登場したのがraise Exceptionですね。これは例外(Exception)というPythonの機構で、実行時のエラーに対処するために用意されているものです。ファイルが存在しない場合を考慮せず実行すれば、openFileNotFoundError: [Errno 2] No such file or directory: '指定したファイル名'という例外が発生します。でも、今回のプログラムでは、「指定されたファイルは存在しません。[ファイル名]」と丁寧に教えてあげることにしました。大規模なモジュールを開発するときは独自の例外を定義するようですが、今回は規模も小さいので、これでよいでしょう。
このファイルが開けなかった時のようなエラーの一般的な処置として、関数の戻り値を使ってNoneを返すことでエラーを教えることもできますが、予期せぬ不具合になる可能性があるため、このような場合には例外を選ぶ、というのがパイソニックな考え方だそうです(笑)。
ちなみに、どういうときに予期せぬ不具合になるかというと、ファイルサイズが0の時の戻り値判定などですね。if not data:のように書いてしまうと、エラーかデータなしかわからずに処理続行してしドツボにハマる可能性がありそうです。

さて、のこりもうひとつ注目点は、最後の方のif __name__ == '__main__':です。この記述により、モジュールとしてimportしたときにこの行以降は実行されなくなります。ただし、python hexdump.py ...と実行されたときには、このif以降の処理が有効になります。これもPythonでモジュールをスタンドアロンとしてスクリプトを実行する準標準イディオムなので、覚えておくとよいでしょう。最後にこのスクリプトを書くことで、スタンドアロンとして実行できるだけでなく、モジュールの使い方の具体例を示すことができるので、積極的に書いた方がいいのかな、と思います。

いい感じにできました、ということで実行してみましたところ、スタンドアロンの実行はこれまで通りで問題ありませんでした。しかし、関数として実行した結果はいまいちです。こんな感じです。

>>> import hexdump
>>> hexdump.hexdump('hexdump.py')
'66 72 6f 6d 20 6f 73 2e 70 61  74 68 20 69 6d 70 6f 72 74 20 \n65 78 69 73 74 73 0d 0a 0d 0a  64 65 66 20 68 65 78 64 75 6d \n70 28 
・・・(中略)・・・
\n20 20 65 78 69 74 28 29 0d 0a  20 20 20 20 70 72 69 6e 74 28 \n68 65 78 64 75 6d 70 28 61 72  67 76 5b 31 5d 29 29 0d 0a 0d \n0a '
>>>                                                                                                                     

間の空白や改行コード、自由に指定したいですよね。それに、開始と取得バイト数などがあるといいかも。と、いうことで、指定可能に変更してみます。

def hexdump(filename, separator=" ", enterper=20, separateper=10, start=None, end=None):
    '''処理:ファイルの内容を16進数でダンプする
引数:separator 区切り文字 初期値空白 バイトごとに挿入する
   enterper 何バイトごとに改行するか(0は改行なし)
   separateper 何バイトごとに追加で区切り文字を入れるか(0は追加なし)
   start 何バイト目からスライスの開始値
   end 何バイト目までスライスの終了値
    '''

パラメータがありすぎて、ちょっと使い勝手がよくないですけど、その他の変更箇所はわりと限定されていて、以下のとおりです。

    data = data[start:end]
    for i, x in enumerate(data, 1):
        output += f'{x:02x}' + separator
        if enterper > 0 and i % enterper == 0:
            output += "\n"
        elif separateper > 0 and i % separateper == 0:
            output += separator

ここまで完全にスルーしてましたけど、「%」演算子は余りを求める演算子でモジュロといいます。0で割り算しようとするとエラーになってしまいますので、enterper > 0を入れることで意味のないマイナス値とエラーを回避しています。ちなみにPythonの論理演算は、左から順番に評価していきます。enterper > 0を評価してからi % enterper == 0を評価する、ということです。また、enterper > 0がFalseになる場合はその時点で式全体がFalseであるということが確定するので、次の式の評価は必要ないため行いません。このため、評価してほしい順番に式を書くよう気をつけましょう。

改良点はまだまだありますが、ここまででいったんモジュールへの置き換えは完了とします。

Pythonで16進数ダンプするプログラムを作る

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

 Python勉強会、ひととおりの要素について、説明させていただきました。しかし、実際にプログラミングしないと、なかなか身につきません。ということで、「バイナリファイルの内容を16進数でダンプするプログラムを作る」という課題を用意しました。そんなに難しくはないのですけど、いろいろと考えなければならないことがあり、取り組みがいのある課題ではないでしょうか。

【課題】ファイルの内容を16進数でダンプする
ファイル名:hexdump.py
使い方:python hexdump.py ダンプしたいファイル名

 お時間のない方に、出来上がり結果は以下の通りです。

from sys import argv
from os.path import exists

process = '処理:ファイルの内容を16進数でダンプする'
howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'

if len(argv) != 2:
    print(howtouse)
    exit()

if not exists(argv[1]):
    print(f"指定されたファイルは存在しません。 [{argv[1]}] ")
    exit()

with open(argv[1], 'rb') as f:
    data = f.read()

for i, x in enumerate(data, 1):
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

 さて、まずは、引数の「ダンプしたいファイル名」が指定される、というところから。これは、sysモジュールのargvを使います。ちょっとテスト的に、以下のように入力してみましょう。

from sys import argv
print(len(argv), argv)

実行結果は以下のようになります。

(base) C:\work\friStudy>python hexdump.py test
 2 ['hexdump.py', 'test']

 sys.argvは、実行時引数が格納されていて、一つ目が自分自身、以降は指定された値が順番に格納されてきます。引数は「ダンプしたいファイル名」ということなので、len(argv) == 2はチェックする必要がありますね。次に指定されたファイルが存在することをチェックしましょう。ファイルが存在するかどうかは、os.path.existsを使って確認することができます。ここまでで、以下の通りです。

from sys import argv
from os.path import exists

process = '処理:ファイルの内容を16進数でダンプする'
howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'

if len(argv) != 2:
    print(howtouse)
    exit()

if not exists(argv[1]):
    print(f"指定されたファイル[{argv[1]}]は存在しません。")
    exit()

 ここで何気なく登場しましたが、f'使い方:python {argv[0]} ダンプしたいファイル名'のように、文字列の引用符の前にfがついていると、文字列中の{...}の中に記載の変数を展開して文字列にしてくれます。この例の場合、argv[0]hexdump.pyになります。

 ここから本番です。指定されたファイルを開いて内容を16進数でダンプしていきます。1バイトずつ処理していきますので、文字列ではなく、バイトオブジェクトとして処理する必要がありますので、openのモードは'rb'で指定します。rreading、読み込み専用で、bbinary、バイナリですね。通常のプログラミングならopenしてcloseということになりますが、ここはパイソニックにwith句を使います。これで何かあっても必ず自動でcloseが呼び出されることが保証されます。

with open(argv[1], 'rb') as f:
    data = f.read()

これで、ファイルの内容をすべて取得できます。あとは、出力ですね。1バイトずつ、16進数で出力しましょう。スペースをひとつずつあけて、20バイトで改行、10バイトでスペースふたつ、出力するようにしてみましょう。

for i, x in enumerate(data, 1):
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

ここで何気なくenumerateが登場しました。基本的に、Pythonのforループはシーケンスと呼ばれる繰り返し要素を持ったもの(ここではdata)を繰り返して処理するために使います。対象となる要素をひとつずつ処理していくときにはdatafor x in data:と指定するだけで済むのです。では、enumerateは何のために必要なのでしょうか。今回のように、要素を数えていって、20個目で改行したいとか、別のリストの同じ位置の要素を一緒に処理したいときに使用できます。上記のように指定すると、インデックス(順番)と要素を同時に取得することができるようになります。いわゆるイディオムだと思って覚えておきましょう。ちなみに、今回指定した第二引数の1は、インデックスの初期値で指定しない場合は0から始まることになります。もちろんenumerateを使わなくても以下のように記載できますが、使った方がよりパイソニックだそうです。二行少なく書けるからかな?(笑)

i = 0
for x in data:
    i += 1
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

ちなみに、printend=' 'は、出力の最後に追加される文字で、初期値は改行コードになります。end=' 'を指定することで、改行が抑止されて空白が追加されます。このため、20行ごとの改行のためにprint()が記述されています。また、f'{x:02x}'の記載ですが、上述したように変数xの値を文字列に展開してくれますが:02xのようにコロン(:)を付けることで書式設定することができます。この指定で、前ゼロ付きの2桁、16進数で出力します。

これらを実行すると、以下のように結果が出力されます。

(base) C:\work\friStudy>python hexdump.py hexdump.py
66 72 6f 6d 20 73 79 73 20 69  6d 70 6f 72 74 20 61 72 67 76
0d 0a 66 72 6f 6d 20 6f 73 2e  70 61 74 68 20 69 6d 70 6f 72
74 20 65 78 69 73 74 73 0d 0a  0d 0a 70 72 6f 63 65 73 73 20
(… 中略 …)
20 20 20 70 72 69 6e 74 28 29  0d 0a 20 20 20 20 65 6c 69 66
20 69 20 25 20 31 30 20 3d 3d  20 30 3a 0d 0a 20 20 20 20 20
20 20 20 70 72 69 6e 74 28 65  6e 64 3d 27 20 27 29 0d 0a
(base) C:\work\friStudy>

これでいろいろなファイルを16進数でダンプ出力できるようになりました。

今回はここまでです。

GitLabにプロジェクトをつくってみた

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

 先日、tkinterのサンプルをつくってみようと思ったところに、最近使い始めたGitLabが使えないかな、ということを思いついて、ちょっとやってみました。gitは分散型のバージョン管理システムを実現するためのプログラムで、GitLabは主にそのgitのリポジトリマネージャを提供しているサービスです。早速つくってみます。 https://gitlab.com/projects/new へ移動して、以下を順番に入力します。あ、ご自分のアカウントは、適当に作ってくださいね。

 URLにアクセスすると、上記のような画面になります。タブで「Blank project」が選ばれた状態になっています。他のテンプレートから作成する方法や、インポートしてくる方法や、外部のリポジトリを利用する方法などがあるようですが、どれもぴったりくるものがなかったので、初期値の「Blank project」のままでやることにします。
Project name:今回は「tkinter example project」としました。自由に入力できます。
Project URL:アカウントに応じた初期値が入っています。
Project slug:「Project name」を入力すると自動入力されました。
Project description(optional):「tkinterのサンプル集」と入力しました。
Visibility Level:は「Private」を選択。あとからProject Settingsで変更できます。
Initialize repository with a README:チェックします。初期化してください。
さ、実行しましょう。「Create project」をクリック。

おお、なんと、一瞬でできてしまいました。

ええと、まずは、先日作ったサンプルを登録してみましょうか。最近インストールしたのですけど、Git for Windowsを使ってやってみます。(必要な方は、ここからインストールしてください。インストールすると、BASHエミューレーションされたコマンドランツールとGUIのツールがインストールされます。Windowsエクスプローラーの右クリックのメニューにも機能が追加されます。)エクスプローラでリポジトリのクローンを作成するフォルダに右クリックすると以下のようなメニューがポップアップします。

「Git GUI Here」を選択します。

Git Guiが起動します。

先ほどリポジトリサービスを作成しましたので「Clone Existing Repository」を選択します。

Source Locationに作成したリポジトリを指定、Target Directoryにローカルのフォルダを指定します。
Source Location: https://gitlab.com/TMsTone/tkinter-example-project
Traget Directory:C:\Work\tkinter_example
「Clone」を実行します。(tkinter_exampleフォルダをつくってから実行すると、そのフォルダは既に存在します、と、叱られました。削除して再実行しようとしてもGit Guiがつかんでいるらしく削除できなかったので、Git Guiを終了して、フォルダを削除してから再実行しました。みなさんはフォルダがない状態で実行しましょう。)

指定したフォルダにサンプルのtkinter_example.pyファイルを作成して、Rescanしてみました。すると、Unstaged Changesの一覧にファイルが登場しました。

Gitもあんまりよく分かっていないのですけど、どうやら「Unstaged Changes」から「Staged Changes」に移さなければいけないようですね。と、いうことで、「Stage Changed」をクリックして見ます。

「Stage 1 untracked files?」と、聞いてきましたねぇ。何なのでしょうか、よくわかりませんが、「はい(Y)」をクリックして見ます。きっと、untrakedされるのでしょう。。。

「Staged Changeds」に行きましたねぇ。そして、「Commit」かな?
あ、「Please supply a commit message」と叱られました。コミットメッセージを入力して、「Commit」してみます。

次はやっぱり「Push」ですかねぇ。クリックしてみます。

「Push」をクリックします。

なんと、うまくいったようです。

GitLabの方へ行って、Filesで確認してみたところ、「tkinter_example.py」が追加されていました。
gitの本来の使い方は少し違うのでしょうけど、ひとりで使う分にはこれで充分でしょうか。もう少し慣れてきたら、gitの使い方について書こうかな。

実行時エラー ‘1004’: アプリケーション定義またはオブジェクト定義のエラーです。

今日も見に来てくださって、ありがとうございます。
お仕事で、ちょっとつまずいたので、まとめます。誰かのお役にたつとうれしいです。

 あ、お急ぎの方に、結論から言いますと、Microsoft Excel VBAマクロでセルに正しくない計算式をセットするとこのエラーを発生させてしまいます。
 例:

Range("A1").Value = "=SUM([3:[10)"

 業務でマクロ、普通に作られていますね。ある日、「実行時エラー ‘1004’:」なんて発生して動かなくなると、ショックですよねぇ。このエラー、Google先生に聞いてみたのですけど、どうして発生するのかという条件がありすぎるせいか、原因を特定するのが難しいエラーのようです。通常のエラーだと、マクロが発生個所を教えてくれるのですが、関数内で「On Error GoTo 」が記述されてあったので、このエラー、どこで発生したのか分かりませんでした。なので1行ずつ自分でステップ実行していって、発生個所を特定する必要がありました。
 今回エラーが発生したマクロのイメージは以下のような感じです。商品ごとに毎月の売り上げがあって、1年間の合計欄を作成するようなケースです。

Sub 年間合計欄をセット()
    ' 開始行、開始列から下にある12か月分の合計セル(=SUM())を商品店数分セットします。
    Dim i As Integer
    Dim 商品点数 As Integer
    Dim 開始行 As Long
    Dim 開始列 As String
    Dim 出力列 As String
    Dim 計算式 As String

    商品点数 = 5
    開始行 = 3
    開始列 = "C"
    For i = 1 To 商品点数
        出力列 = Chr(Asc(開始列) + i - 1)
        計算式 = "=SUM(" & 出力列 & Format(開始行) & ":" & 出力列 & Format(開始行 + 12 - 1) & ")"
        Range(出力列 + Format(開始行 + 12)).Value = 計算式
    Next
 End Sub

 ここでのポイントは、「出力列」の算出方法です。「開始列」”C”は、Asc関数で67になります。これは”C”をASCII文字コードに置き換えています。これに順次数字を加えていって、商品点数5の場合、”C”(67)、”D”(68)、”E”(69)、”F”(70)、”G”(71)が出力列として算出されます。できあがる一つ目の「計算式」は、この場合「=SUM(C3:C14)」になります。一見問題なさそうに見えますね。
 問題点は、列が”Z”(90)までは見つかりません。問題は列がその隣にうつった時、”AA”≠(91)のため発生するのです。今回これを回避するためにエクセルの機能を利用して以下のように関数を作成しました。

'*****************************************************************
'*関数名 :Number2Letter
'*機能概要:入力されたカラム位置の数値からカラム文字の英数字に変換します
'*引数   :カラム位置(1~16384)
'*戻り値 :カラム文字
'*****************************************************************
Function Number2Letter(iCol As Integer) As String
    Number2Letter = Split(Columns(iCol).Address(True, False), ":")(0)
End Function

 あるいは、数値をA~Zの26文字で表現する26進数に変換する、と考えると以下のような関数をつくってもよいかも知れませんね。

Function Number2Letter(iCol As Integer) As String
    If iCol < 1 Then Number2Letter = "": Exit Function
    Number2Letter = Number2Letter(Int((iCol - 1) / 26)) & Chr(Asc("A") + ((iCol - 1) Mod 26))
End Function

 これで、スッキリ解決です。

 ちなみに、今回の問題点をわかりやすくするために、単純化して書いてありますけど、実際にはちょっとした落とし穴があって、エラーは以下のような行で発生していました。簡単にしてますけど、”A:A”,”AA:AA”は複雑な変数で記載されていましたよ。

Range("A:A","AA:AA").Value = Range("A:A","AA:AA").Value

 同じ範囲のレンジを代入しているだけなので、エラーが起きる意味が分かりませんよね。今回、対象商品が増えてエラーが発生したので、すぐに「AA」が怪しいな、とあたりを付けて、「Range(“A:A”,”Z:Z”).Value」と「Range(“AA:AA”).Value」に分けて実行したところ、前者は成功して、後者が失敗したのです。それで、エクセルは「AA」セルのコピーを失敗する不具合があるのかな、と、一つ目の落とし穴に落ちたのでした。
 このValueへのセット、実は、文字列の式を値に変換するために代入しているのでした。そのため、文字列の式に誤りがあると、今回のエラーが発生するわけです。同様のエラーを発生させるステップは以下の通りです。文章だと意味がわかりにくいと思いますので実際に実行してみてくださいね。

Sub test()
    Range("A1").NumberFormat = "@" ' 文字列書式に変更
    Range("A1").Value = "=SUM(]3:]14)" ' 正しくない式を代入
    Range("A1").NumberFormat = "General" ' 書式を戻す
    Range("A1").Value = Range("A1").Value ' エラーが発生します
End Sub

 と、いうことで、処理データが増えたときにカラム方向へセルを拡張させるときには、気を付けましょう、というお話でした。

追記

 このページ、ぼくのブログの中では一番アクセスが多いので、きっとこの問題で悩んでいる人は多いのでしょうね。ということで、またしても発生しましたので原因を追記いたします。

 再現するには、イミディエイトウィンドウで、以下のように実行します。

? Sheet1.Cells(0,1).Value
実行時エラー’1004′

 セルの行列の数値を正しくセットしていない場合にも発生します。オフィス2013のエクセルの場合、行の値の範囲は、1~1048577、列の値の範囲は1~16384の制限があるようです。なので、VBAで変数を使って行列をセットしている場合は注意が必要です。

 不覚にもこのエラーがまた出てしまいましたが、原因は、if文の条件ミスでした。変数に値をセットできておらず、0で実行してしまったのでした。みなさんも、気をつけましょう。

さらに追記

 ここまで読んでくださって、ありがとうございます。さて、ここまで読まれたあなたは、問題が解決したでしょうか?いやいや、まだ解決していない、ということでもしよけらばコメントいただけますでしょうか。Excelの記事は数えるほどしかないホームページ ですが、前述しましたとおり、このページは中でもアクセス数が多いページです。訪れられた方の問題が少しでも多く解決するといいな、という気持ちでやっておりますので、上記以外の問題でも解決できるようにしたいと思います。新たな問題、お待ちしております。

PythonでGUI

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

 Pythonを使ったGUIプログラミング、何を使ったらいいのでしょうね。ネットを調べてみると、いろいろとあって悩みます。標準搭載のGUIはTkinterなのですが、ビジネスでちょっと使ってみるGUIだと、これで充分ですね。ということで、helpに記載のあった以下の例題を実行してみます。

import tkinter
from tkinter.constants import *
tk = tkinter.Tk()
frame = tkinter.Frame(tk, relief=RIDGE, borderwidth=2)
frame.pack(fill=BOTH,expand=1)
label = tkinter.Label(frame, text="Hello, World")
label.pack(fill=X, expand=1)
button = tkinter.Button(frame,text="Exit",command=tk.destroy)
button.pack(side=BOTTOM)
tk.mainloop()

でました!
定番の、Hello, Worldです♪

 何から始めるのがいいか、ちょっと迷うところではありますが、チュートリアルとかはもう概ね経験済みなので、tkinterのソースを見てみようと思います。ぼくの環境だとインストールされているフォルダは「C:\ProgramData\Anaconda3\lib\tkinter」でした。このフォルダがモジュール名なので、最初に読み込まれる「__init__.py」をざっと見てみました。基本的には、何をやるにも「self.tk.call(…)」と、呼び出しているようです。self.tkの正体は、というと、最初に「_tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)」のように呼び出されています。「_tkinter」は何者かわかりませんが、最初に「import _tkinter」と読み込まれていました。今使っているのがAnaconda3の環境なので、このフォルダ以下を「_tkinter」で検索してみました。すると、 libsフォルダ、DLLsフォルダ から「_tkinter.lib」「_tkinter.pyd」が検索されました。これらのライブラリを使ってます、ということでしょうね。利用者側としてはひとまずこれくらいの理解でよいでしょうか。

 ライブラリを使ってできることは、tkのライブラリがどうなっているのか知る必要がある、ということですね。ライブラリを参照しに行く前に、とりあえず、定義されているclassについて見ていくことにしましょう。っと、一行ずつコピペしましたが、多いなぁ。内部的なクラス(Internal class)、と、書いてあったのは、線を引いていきましょうか。む~、まだよくわかりませんねぇ。親子関係になっているものは、インデントを付けてみましょうか。

class EventType(str, enum.Enum):
class Event:
class Variable:
  class StringVar(Variable):
  class IntVar(Variable):
  class DoubleVar(Variable):
  class BooleanVar(Variable):
class Misc:
class CallWrapper:
class XView:
class YView:
class Wm:
  class Tk(Misc, Wm):
class Pack:
class Place:
class Grid:
  class BaseWidget(Misc):
    class Widget(BaseWidget, Pack, Place, Grid):
    class Toplevel(BaseWidget, Wm):
      class Button(Widget):
      class Canvas(Widget, XView, YView):
      class Checkbutton(Widget):
      class Entry(Widget, XView):
      class Frame(Widget):
      class Label(Widget):
      class Listbox(Widget, XView, YView):
      class Menu(Widget):
      class Menubutton(Widget):
      class Message(Widget):
      class Radiobutton(Widget):
      class Scale(Widget):
      class Scrollbar(Widget):
      class Text(Widget, XView, YView):
class _setit:
        class OptionMenu(Menubutton):
class Image:
  class PhotoImage(Image):
  class BitmapImage(Image):
      class Spinbox(Widget, XView):
      class LabelFrame(Widget):
      class PanedWindow(Widget):

 インデントを付けてみて、なんとなくわかってきました。画面の要素はWidgetを継承していて、→BaseWidget→Miscとなっているので、Miscが一番の先祖になっているのですね。他に、イベントを管理するためのEventクラスがあって、Variableを継承している何らかの値を保持するクラスがあって、Widegetの中でも見え方に特徴のあるものがXViewやYViewを多重継承して見せ方に特徴を持たせてそう。他のWmはウィンドウマネージャーかなぁ、何となくGUIの全体を管理しそうな、、、そして、Pack、Place、Gridは位置決めに使うクラス、あとは、メニュー用とイメージ用、と、ざっとこんな感じでしょうか。
 それ以外にtkinterのフォルダを見てみました。

colorchooser.py ネイティブのカラーダイアログ(Chooserクラス)
commondialog.py 共通のダイアログ関連(Dialogクラス)
constants.py コンスタント値関連
dialog.py ダイアログ関連(Dialogクラス)
dnd.py ドラッグアンドドロップ関連
filedialog.py ファイル関連のダイアログ関連
font.py フォント関連
messagebox.py メッセージボックス関連
scrolledtext.py スクロールするテキスト関連
simpledialog.py 簡単なダイアログ関連
test テスト用かな
tix.py Tkの拡張ウィジェット(3.6から非推奨。使いません。ttkを使いましょう。)
ttk.py Tk8.5から新しく追加された、テーマ付きウィジェットに関するもの
__init.py
__main.py
__pycache

 全体感は、これで何となくわかりました。あとは、個別の利用方法ですかね。

 と、いうことで、最近gitLaboというサービスを触り始めたので、そちらの勉強もかねてもろもろが分かりやすくなるように簡単なサンプル集でもつくりますかねぇ。動くものがないと、理解できないですよね、きっと。
サンプルをつくっていく中で、いろいろと書きたいことが増えてくると思います。

2021年1月10日 追記

 こちらに、クラス図を追記いたしました。概要を把握するのにどうぞ。

Python Programming 動的にclassを作成

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

 勉強会でPythonの講師をさせていただいているので、ここのところPythonについて思いを馳せるタイミングが増えてきています。最近Pythonを使っていて、おお!と思ったことをちょっと書こうと思います。なんと、クラス作成後に属性を追加することができるのです!すべてオブジェクトにすると、こんなこともできてしまうのですね。ちょっといろいろと確認してみます。

 まずは、何もないクラスを作成。そして、インスタンスを作成します。

>>> class Box:
...     pass
... 
>>> box1 = Box()

なんと、このあと動的にアトリビュートを追加することができるのです。

>>> box1.x = 100
>>> box1.y = 120         
>>> print(box1.x, box1.y)
100 120
>>> 

当然、メソッドも追加できますよね?
ということで、やってみます。まず関数を定義して、代入して、実行してみます。

>>> def getpos(self):
...     print(self.x, self.y)
...
>>> box1.getpos = getpos
>>> box1.getpos()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: getpos() missing 1 required positional argument: 'self'
>>> </module></stdin>

おっと、うまくいきませんね。ちなみに、この「self」というのは、クラスのメソッドがインスタンスになった時、関数実行時に一つ目のパラメータとしてインスタンスが渡されるために必要になる引数です。ホントはどんな名前でもいいのですけど慣例として「self」とすることになっています。
あ、ここまで書いて気づきました。インスタンスに関数を定義したのがいけないのかも知れません。Boxクラスの方へセットしてみます。

>>> Box.getpos = getpos
>>> box2 = Box()
>>> box2.x = 300
>>> box2.y = 320
>>> box2.getpos()
300 320
>>>              

できました!
このように、クラス作成後に動的にクラスを変更できてしまうので、プログラムを使ってコーディングすることができますね。ちなみに、アトリビュートは文字列を使ってセットできることを確認しました。dir()関数を使って、定義したクラスの中身を調べてみました。

>>> dir(Box)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'getpos']
>>> 

名前からアトリビュートっぽいのを探ります。以前いろいろと探して「__dir__」に含まれているのを知っていたので、中を見てみましょう。

>>> dir(Box.__dict__)
['__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'copy', 'get', 'items', 'keys', 'values']

ええと、ディクショナリークラスの中身のようですね。見方が違いました。きっと、こうですね。

>>> Box.__dict__
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Box' objects>, '__weakref__': <attribute '__weakref__' of 'Box' objects>, '__doc__': None, 'getpos': <function getpos at 0x0000029DA5021E18>})

先程追加した関数getposはありましたが、xとyがありませんね。あ、、、そういえばインスタンスの方にセットしただけでした。では、インスタンスの方を確認しましょう。

>>> box1.__dict__
{'x': 100, 'y': 120, 'getpos': <function getpos at 0x0000029DA5021E18>}

そうそう、こんな感じです。
では、文字列の’x’と’y’を使って、クラスの方へアトリビュートと初期値をセットしてみます。

>>> setattr(Box,'x',10)
>>> setattr(Box,'y',20)
>>> dir(Box)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'getpos', 'x', 'y']
>>> box3 = Box()
>>> box3.getpos()
10 20
>>>

はい、見事、xとyがクラスに追加されて、メソッドが最初から利用できるようになりましたね。オブジェクトの場合は「__dict__」を普通のdictと同じようにしてアトリビュートを追加することができたのですけど、クラスの場合の「__dict__」はmappingproxyになっていて読み込みのみでしたので、setattrを使う必要がありました。ちなみに、中を見てみるとどうなっているかというと、

>>> Box.__dict__
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Box' objects>, '__weakref__': <attribute '__weakref__' of 'Box' objects>, '__doc__': None, 'getpos': <function getpos at 0x0000029DA5021E18>, 'x': 10, 'y': 20})
>>>

こんな感じです。オブジェクトの方にアトリビュートを追加するもう一つの方法は以下の通りです。

>>> box3.__dict__['z'] = -100
>>> box3.__dict__
 {'z': -100}
>>>

おっと、少し想定と違いました。xとyがいませんね。クラスで定義したアトリビュートはここには入らない、ということでしょうか。以下の通り、値は取り出せました。

>>> box3.x
10
>>> box3.y
20
>>> box3.z
-100
>>>

ちょっと、xに値を代入したらどうなるかやってみます。

>>> box3.x = -10
>>> box3.__dict__
{'z': -100, 'x': -10}
>>> 

なるほど、思った通りですね。クラスで定義した値が変更されていなければインスタンスごとにその値を持つ必要がない、ということですね。

そういえは、関数を文字列から作成して組み込むのはどうやるのだろうか、と、気になってまいりましたが、長くなってきたので今回はこれくらいで。

WordPressでMarkdownを使うには

International Arrivals

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

 先日Google Colaboratoryを発見した、という記事を書きました。Pythonのスクリプトを実行したり、簡単なドキュメントをMarkdown形式で記載できるツールです。これまでにこのブログを書いている中で、スクリプトとか、もうちょっと書きやすくならないかなぁ、と不便を感じていました。Colaboratoryでは、Markdownで美しく書くことができていいなぁ、そういえば、WordPressはいろんなプラグインを入れられるから、もしかしたらMarkdownも使えるようにできるかも、と、調べてみました。
 ちなみに、Markdownとは、テキストだけでHTMLを出力できるように考えられた記法のことです。例えば、行の先頭に「# 」と記載することで、HTMLの見出しである<H1>タイトル</H1>を表現します。これらの記法を少し学べばテキストだけで美しい文章が作れ、テキストだけ見ても何となく構成がわかる、というのが特徴です。

 で、Google先生に聞いてみました。なんと、ぼくの環境では、Gutenbergが入っているので、すでに使えるようになっているそうです。早速やってみます。

# タイトル

 ええと、見出しになりませんねぇ。(汗)(Markdown記法では、先頭に「# 」が付いた行はHTMLで言うところのH1の見出しになります。「## 」「### 」と、個数が増えると順に小さくなっていきます。)さらにGoogle先生に聞いてみます。。。おお、シャープを入力した後にスペースを入力せよ、と書いてありました。実際にやってみましたが、なぜかできない。いろいろ試した結果、やっとできました。シャープ1個の時は、ダメで、2個以上だと動作するようです。

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

そして、バッククオートを三つ書いてエンターキーを押すと「```」
このような、コードを入力モードになりました。
  • 「- 」とスペースで箇条書きがスタートします。
  • エンターで次の箇条書きになります。

 なるほど、ブロック単位で設定していくようです。コードブロックは先頭行で「``` 」だし、箇条書きは先頭行で「- 」という感じですね。ただ、ブロック毎に変換されるので、これは、編集中に出てくるボタンを使ってやるのとあんまり変わりませんね。文章の編集にGutenbergを使っていると、Markdownはうまく使えないようです。編集効率を上げるためにMarkdown形式をバッチリ使いたいなら、他の方法を検討しないとダメですね。Markdownが使えると何となくエンジニアっぽい気がするので、Markdownのプラグインを探してきて(あれば)入れてみますかねぇ。。。

【追記】Markdownで記載したものをコピペすると、いい具合に使えることがわかりました。ただ、そうすると、テキストと記事を二重管理することになりそうだなぁ、と、いうことでやっぱりプラグインかな、と、思っています。