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

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の記事は数えるほどしかないホームページ ですが、前述しましたとおり、このページは中でもアクセス数が多いページです。訪れられた方の問題が少しでも多く解決するといいな、という気持ちでやっておりますので、上記以外の問題でも解決できるようにしたいと思います。新たな問題、お待ちしております。