PythonのGUIアプリの勉強を兼ねてCSV viewer(CSVビューア)を作成しました。
GUIにはTkinterを、ドラッグアンドドロップ対応にTkinterDnD2を使用しました。
また、アプリも作成し、アプリにファイルをドラッグアンドドロップすると、アプリが起動しファイルを表示するようにしました。
TkinterDnD2 の使い方(簡単なパス情報の取得方法も)、アプリの作り方をサンプルコードで説明します。
目次
- ◆アプリのサンプル画像
- ◆機能・特長
- ◆必要なパッケージ
- ◆ドラッグアンドドロップの実装(TkinterDnD2の使い方)
- ◆Tkinterの使い方
- ◆画面の作成
- ◆タブの実装(Notebookウィジェットの使い方)
- ◆表の実装(Treeviewウィジェットの使い方)
- ◆スクロールバーの設置
- ◆ファイル選択ダイアログ
- ◆CSV ファイルの操作
- ◆全体のソース
- ◆バイナリ(アプリ)
- ◆さいごに
- ◆更新情報
- ◆参考
◆アプリのサンプル画像
▼複数ファイルをタブ表示します
▼タブごとに行の高さを自動調整します
▼TSVも表示可能です(対象外のファイルをドラッグアンドドロップするとコメントが出ます)
▼対象のファイルがない場合はコメントが出ます
◆機能・特長
- CSV,TSV,TEXTファイルを読み込みその内容を画面に表示
- 複数ファイルをタブ表示
- タブごとに行の高さを自動調整
- ドラッグアンドドロップでファイルを指定可能(TkinterDnD2使用)
- exeにドラッグアンドドロップでファイルを指定可能(TkinterDnD2使用でも)
◇ドラッグアンドドロップに対応
ファイルをドラッグアンドドロップして表示できます。
対応には、TkinterDnD2 を用います。
これは、Tkinter にドラッグアンドドロップ機能を付加するライブラリです。
詳しくは「ドラッグアンドドロップの実装」を参照
◇画面表示の特徴
画面表示には次の特徴があります。
◇Tkinterの使用ウィジェット
Tkinter がサポートする GUI の部品をウィジェット (widget) と呼びます。
ここでは次のウィジェットを使用しています。
これらの使い方も説明します。
- セルの表示には Treeview ウィジェットを Listview 形式で使用
- タブの表示には Notebook ウィジェットを使用
- スクロールバーの表示には Scrollbar ウィジェットを使用
- 1行おきに背景色を設定するには tag 機能を使用
◆必要なパッケージ
◇必要な Python パッケージ
- TkinterDnD2
◆ドラッグアンドドロップの実装(TkinterDnD2の使い方)
GUI に対してドラッグアンドドロップを実現するには、Tkinter 標準では複雑な対応になるため、ライブラリ TkinterDnD2 を使用します。
TkinterDnD2 は完全に tk をラップしているので TkinterDnD2 の Tk インスタンスを作成すれば、Tk がそのまま機能します。
今回は、ファイルをドラッグアンドドロップしてファイル名を取得し処理する使い方を実装しています。
これに関わる使い方を説明します。
◇使い方の基本
- インストール:
pip install tkinterdnd2
- インポート:
from tkinterdnd2 import *
(この方が安全と仕様にあります) - インスタンス作成:
TkinterDnD.Tk()
Tkinter で tk.Tk()としていたところをこれに置き換えるだけです。 - ドロップを受け取るウィジェットで受け取りを登録:
drop_target_register
- ドロップ後に実行するメソッド(コールバック関数)を登録:
dnd_bind
◇ドラッグアンドドロップの設定メソッド
ウィジェットに追加された主なメソッド
TkinterDnD2 は Tkinter をラップしているので、Tkinter で用意しているウィジェットを使用し、さらに TkinterDnD2 で拡張されたメソッドを利用できます。
以下は、ウィジェットオブジェクトに拡張されたメソッドの一部です。
dnd_bind(self, sequence=None, func=None, add=None)
ウィジェットにドラッグアンドドロップ動作をバインド
シーケンスと動作時に起動する関数を指定drop_target_register(self, *dndtypes)
ドロップを受け取るウィジェットとして登録- 【dndtype】
DND_FILES
(ファイルリスト),DND_TEXT
(テキスト) など(プラットフォーム依存しないもの)
- 【dndtype】
drag_source_register(self, button=None, *dndtypes)
ドラッグを開始するウィジェットとして登録- 【dndtype】
DND_FILES
(ファイルリスト),DND_TEXT
(テキスト) など(プラットフォーム依存しないもの) - 【button】ドラッグするときのマウスボタン(1:左(デフォルト)、2:中、3:右)
- 【dndtype】
drop_target_unregister(self)
ウィジェットのドロップを受け取る登録を止めるdrag_source_unregister(self)
ウィジェットのドラッグを開始する登録を止める
◇パス取得 - イベントクラスに追加された属性データ(event.data)
TkinterDnD2 で Tkinter のイベントクラスにも拡張があります。
その中の data 属性でドラッグアンドドロップされたファイルの情報が取得できます。
◎パス情報 - データ仕様
ファイルをドラッグアンドドロップした場合、ファイルのパスが文字列で返ります。文字列には複数のパスが含まれます。
- 【文字列構文】
パス名1 パス名2 パス名3 ・・・
【パス名構文】パス名 = パス名 | {パス名}
- 「{パス名}」となるのは空白を含む場合
※日本語が入る場合も同様みたいですが不明確です。
- 「{パス名}」となるのは空白を含む場合
- 例
'{C:/temp/persons_email_db - コピー.csv} C:/temp/persons.csv'
◎パス情報のタプル化 - データの分割
取得できるデータは複数のパスを含む文字列なので一つずつのパスに分けないと扱いにくいです。
文字列をパスごとのタプルに分割するメソッドがあります。
- 【メソッド】
widget.tk.splitlist(str)
【戻り値】パス名のタプル
※widget はウィジェットオブジェクト
例 :t = widget.tk.splitlist(event.data)
結果の例:('C:/temp/persons_email_db - コピー.csv', 'C:/temp/persons.csv')
◇本アプリでのドラッグアンドドロップ対応
本アプリでは、複数ファイルを選択してドラッグし、アプリ画面上の任意の位置にドロップするとファイルの中身が表示されるように対応します。
【処理】
- ウィジェットにドロップで動作するメソッドをバインド
- コールバック関数の処理
- ドロップされたファイル情報をパスのタプルに変換
event の有無で通常呼び出しと区別する
- ドロップされたファイル情報をパスのタプルに変換
【コード】
▼インスタンス作成とバインド
root = TkinterDnD.Tk() # トップレベルウィンドウの作成 tkinterdnd2の適用 root.drop_target_register(DND_FILES) root.dnd_bind("<<Drop>>", listview.open_csv)
※本アプリではトップレベルウィンドウに対してドラッグアンドドロップの設定を行っていますが、個別のウィジェットでも可能です。
▼コールバック関数でイベントで取得したファイル名を解析
def open_csv(self, event=None): # DnD対応 if event: # DnDのファイル情報はevent.dataで取得 # "{空白を含むパス名1} 空白を含まないパス名1"が返る # widget.tk.splitlistでパス名のタプルに変換 # list_csv_pathはListboxウィジェットのオブジェクトです self.file_paths = self.list_csv_path.tk.splitlist(event.data)
◇pyinstaller使用時の注意
pyinstallerを使う場合、次の対応が必要です。
- hook-tkinterdnd2.pyをGithubから取得
リンク:GitHub - pmgagne/tkinterdnd2: Tkinter native drag and drop support for windows, unix and Mac OSX.
hook-tkinterdnd2.pyを開いて表示されている内容をコピーして作成
ファイル名はhook-tkinterdnd2.py - hook-tkinterdnd2.py を pyinstaller を実行するフォルダへコピー
- pyinstallerに次のオプションを追加
--additional-hooks-dir .
例pyinstaller -F -w myproject/myproject.py --additional-hooks-dir .
根拠となる説明文
If you want to use pyinstaller, you should use the hook-tkinterdnd2.py file included. Copy it in the base directory of your project, then:
pyinstaller -F -w myproject/myproject.py --additional-hooks-dir=.
tkinterdnd2 · PyPI
◆Tkinterの使い方
Tkinter(ティーキンター)は Python が標準で提供している GUI パッケージです。
他のプログラムの開発ツールで提供されているようなルック&フィールな GUI ツールではありません。
従って、画面を構成するには、すべてコードを書く必要があります。
Tkinter を使用するには import が必要です。
import tkinter as tk
慣例的に tk と別名を付けるようです。
◇初歩的な使い方
ウィンドウを出すだけのサンプルです。
Python を起動して次の命令を入れるとウィンドウが出てきてアプリらしさをちょっとだけ体験できます。
import tkinter as tk #tkと省略することが慣例のようです win = tk.Tk() #ウィンドウの作成と表示 win.title("Hello, World") #タイトル設定 win.geometry("400x300") #ウィンドウサイズ設定 lbl = tk.Label(win, text="ラベル") #ラベルウィジェットの作成 lbl.pack() #ラベルウィジェットの配置 win.mainloop() #イベント待ち
◇使用ウィジェット
ウィジェットは基本的にインスタンスを作成し、表示テキストや表示方法などを指定して使用します。
今回使用しているウィジェットです。
パッケージ | ウィジェット名 | 用途 | 見た目 |
---|---|---|---|
tkinter | Frame フレーム |
ウィジェットの受け皿 | |
tkinter | Label ラベル |
文字の表示 | |
tkinter | Listbox リストボックス |
選択可能なテキストを表示するボックス | |
tkinter | Button ボタン |
押すと処理が動くボタン | |
tkinter | Scrollbar スクロールバー |
画面をスクロール | |
tkinter.ttk | Combobox コンボボックス |
選択可能なテキストを表示するプルダウンボックス | |
tkinter.ttk | Treeview ツリービュー |
ツリー表示やリスト表示 ここではリスト表示で使用 |
|
tkinter.ttk | Notebook ノートブック |
タブ切り替え表示 |
◎ウィジェットに変数を関連付ける
ウィジェットの値と連動して変更される変数をウィジェット変数と呼びます。
今回は、ファイルパス用リストボックス、文字コード選択用コンボボックス、メッセージ表示用ラベルにウィジェット変数を関連付けます。
ウィジェット変数とウィジェットを関連付けるには、ウィジェットの属性 variable, textvariable にウィジェット変数を指定します。
ウィジェット変数のコンストラクタに value= 引数で初期値を与えられます。
ウィジェット変数の値の読み取りは get() メソッドで、値の書き込みは set() メソッドで行います。
- ウィジェット変数 (抜粋)
- StringVar
- IntVar
他に、DoubleVar, BooleanVar があります。
【コード】
self.var_csv_path = tk.StringVar(value="") # リストボックス height=負 で全体表示 self.list_csv_path = tk.Listbox(parent, height=-1, listvariable=self.var_csv_path) self.btn_f_sel = tk.Button(parent, text="ファイル選択", command=self.select_files) self.lbl_encode = tk.Label(parent, text="文字コード:") self.var_encode = tk.StringVar(value="") self.cbb_encode = ttk.Combobox(parent, values=["utf_8", "cp932"], width=5, textvariable=self.var_encode) self.cbb_encode.current(1) # 第1要素を選択状態にする self.msg = tk.StringVar(value="msg") self.lbl_msg = tk.Label(parent , textvariable=self.msg , justify=tk.LEFT , font=("Fixedsys", 11) , relief=tk.RIDGE , anchor=tk.W)
◇画面構成 - フレーム構造
プログラムを作るにあたって、どのような画面にするか決めなくてはなりません。画面構成を考えるということですね。
画面を構成するというのは、GUI 部品(ウィジェット)を画面に配置することです。
ウィジェットは設定したテキストに合わせてサイズが決まるので、そのまま使用しても画面構成上問題ないものと、広げないと見栄えが良くないものが出てきます。
ウィジェットを広げる場合、他のウィジェットとの位置関係で思ったように配置できないことがあります。
そのようなときに Frame ウィジェットを使用して区分けをして希望する位置に配置します。
引き出しの整理に使うトレイのようなイメージでしょうか。
今回の画面構成です。
- u_frame(frame)
- b_frame(frame)
- note(Notebook)
- frame1(Frame)
- treeview1(Treeview)
- h_scrlbar(Scrollbar)
- v_scrlbar(Scrollbar)
- frame1(Frame)
- note(Notebook)
※「u_frame」などはプログラムで使用した変数名です。
※図形の中の矢印はこの後説明するオプション fill の方向です。
入力をする部分と出力をする部分で大きくフレームを分けています。
結果を表示する部分は余った領域をすべて使用してできるだけ大きく表示させるためです。
◇ウィジェットの配置 - pack
ウィジェットはインスタンスを作成した後に配置 (pack) をしないと表示されません。
配置には他に grid と place がありますが、基本的な pack を使用します。
pack とは、スーツケースに荷物をパッキングするようなイメージなんじゃないかなと勝手に思っています。
ウィジェットを pack すると side オプションで指定された方向から配置します。
その時に配置した方向と直行する方向には可能な限りの領域を割り当てます。
例えば、デフォルトの TOP で配置すると直行する横方向に可能なだけ領域を確保します。横一杯になるということですね。
ただし、これは領域の話で、実際のウィジェットの表示は他のオプションに依ります。
【構文】
ウィジェット.pack(オプション1 = 設定値, オプション2 = 設定値,・・・)
【主なオプション】
オプション 説明 設定値 side 次の方向から詰め込む TOP:上(デフォルト)、BOTTOM:下、LEFT:左、RIGHT:右 fill 割り当て領域内での引き伸ばし NONE:なし(デフォルト)、X:水平方向、Y:垂直方向、BOTH:両方向 expand 割り当て領域の詰め込み方向への拡大 True:する、False:しない(デフォルト)
その他のオプション
オプション | 説明 | 設定値 |
---|---|---|
after | 指定ウィジェットの後に配置 | 他のウィジェット |
before | 指定ウィジェットの前に配置 | 他のウィジェット |
anchor | 割り当て領域内の配置位置 | NW,N,NE,W,CENTER,E,SW,S,SE(方角) |
in | 配置する親 | 親 |
ipadx,ipady | 内部に空ける間隔 | 間隔(単位無しはピクセル) |
padx,pady | 外側に空ける間隔 | 間隔(単位無しはピクセル) |
pack で「よく表示されない」という問題が発生しますが、私もそうでした、pack の順番を変えてあげることで解決する場合があるようです。
必ずではないようですが、配置の時に次のことを考慮しておくと良いみたいです。
- expand オプションを True に指定して pack するウィジェットは、後から pack する。
ウィジェットの pack についてはこちらのサイトが詳しいです。
◇動作をウィジェットに割り当てる - バインド
ウィジェットで何か操作した場合にそれに連動して動作するメソッドを割り当てることができます。
この動作(イベント)とメソッドを割り当てることをバインドと呼びます。
イベントには、キー入力可能なウィジェットでキー入力した場合やマウス操作した場合、コンボボックスで選択肢を選択した場合などがあります。
◎キーバインド
コンボボックスはエントリーを継承しているのでキー操作後に動作する処理を割り当てられます。
コンボボックスで Enter を押した時にCSV ファイルを読み込むようにバインドします。
【構文】
ウィジェット.bind(シーケンス, コールバック関数, 追加フラグ)
引数
self.cbb_encode.bind("<Return>", self.after_change_encode) def after_change_encode(self, event=None):
※引数が必要な場合はラムダ式を使用します。(詳細は省略)
◎コンボボックス
コンボボックスでは、選択肢を選択した時に動作する処理を割り当てられます。
bind メソッドで関数を指定します。
その時のシーケンスは、<<ComboboxSelected>>
です。
self.cbb_encode.bind("<<ComboboxSelected>>", self.after_change_encode) def after_change_encode(self, event=None):
※引数が必要な場合はラムダ式を使用します。(詳細は省略)
◆画面の作成
先ほどの画面構成を踏まえて画面を作成します。
【処理】
Frame の作成
上側用:入力用テキストボックスや実行用ボタン用
下側用:Notebookとその中のTreeview とスクロールバー用Frame の配置 (pack)
上フレームの中身の作成 (
create_input_frame
メソッド)
※Notebook の中身の作成は「タブの実装」で解説します。
【コード】
▼フレームの作成
self.u_frame = tk.Frame(bg="white") # 背景色を付けて配置を見る self.b_frame = tk.Frame(bg="green") # 背景色を付けて配置を見る self.note = ttk.Notebook(self.b_frame) self.u_frame.pack(fill=tk.X) self.b_frame.pack(fill=tk.BOTH, expand=True) self.note.pack(fill=tk.BOTH, expand=True) self.create_input_frame(self.u_frame)
▼上フレームの中身の作成
def create_input_frame(self, parent): """ 入力項目の画面の作成 上段:入力ファイルパス、ファイル選択ボタン、開くボタン、文字コード選択 下段:メッセージ """ self.lbl_csv = tk.Label(parent, text="CSV:") self.var_csv_path = tk.StringVar(value="") # リストボックス height=負 で全体表示 self.list_csv_path = tk.Listbox(parent, height=-1, listvariable=self.var_csv_path) self.btn_f_sel = tk.Button(parent, text="ファイル選択", command=self.select_files) self.lbl_encode = tk.Label(parent, text="文字コード:") self.var_encode = tk.StringVar(value="") self.cbb_encode = ttk.Combobox(parent, values=["utf_8", "cp932"], width=5, textvariable=self.var_encode) self.cbb_encode.current(1) # 第1要素を選択状態にする self.msg = tk.StringVar(value="msg") self.lbl_msg = tk.Label(parent , textvariable=self.msg , justify=tk.LEFT , font=("Fixedsys", 11) , relief=tk.RIDGE , anchor=tk.W) self.lbl_msg.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) # 先にpackしないと下に配置されない self.cbb_encode.pack(side=tk.RIGHT, fill=tk.Y) self.lbl_encode.pack(side=tk.RIGHT, fill=tk.Y) self.lbl_csv.pack(side=tk.LEFT, fill=tk.BOTH) self.list_csv_path.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.btn_f_sel.pack(side=tk.RIGHT) # bind self.cbb_encode.bind("<<ComboboxSelected>>", self.after_change_encode) self.cbb_encode.bind("<Return>", self.after_change_encode)
◆タブの実装(Notebookウィジェットの使い方)
Notebook ウィジェットはタブ切り替えを提供するウィジェットです。
Notebook ウィジェットは tkinter.ttk パッケージに入っているので使用には import が必要です。
import tkinter.ttk as ttk
慣例的に ttk と別名を付けるようです。
この章では次の内容について説明します。
◇タブの削除 - forget
既に存在するタブがある場合、forget
メソッドで削除できます。
引数にタブを指定します。
タブ指定の一つに CURRENT
があり、現在選択中のタブを指します。
- 【構文】
notebook.forget(タブ指定)
- サンプル
【構文】notebook.forget("current")
- タブ指定(抜粋)
n
:0からタブ数の間の整数textオプションの値
:タブ名"current"
:現在選択されているタブ
【コード】
次のコードは、全タブのクリアを行います。
tabs()
メソッドは全タブの名前がリストで返ります。
# notebookの既存のタブを削除 while self.note.tabs(): self.note.forget("current")
◇タブの追加 - add
タブの追加には、add
メソッドを使用します。
引数に追加するウィジェットを指定します。通常、Frame にすることが多いようです。
オプションとしてタブ名を指定する text=
がよく使われます。
- 【構文】
notebook.add(ウィジェット, オプション)
- サンプル
【構文】notebook.add(frame, text=タブ名)
- オプション
text
:タブに表示されるテキスト
【コード】
parent.add(frame1, text=tab_name)
◇Notebookのタブの画面作成
画面構成にあるようにタブの中身を作成します。
Frame を用意して Treeview とスクロールバーを作成します。
スクロールバーは縦横を作成します。
【処理】
- Notebookに追加するフレームの中身の作成 (
create_tree_frame
メソッド)- タブごとのスタイルの設定
- フレームの作成
- Treeviewの作成
- スクロールバーの作成
- ウィジェットの配置 (pack)
【コード】
▼タブ用フレームの中身の作成
def create_tree_frame(self, parent:ttk.Notebook, tab_name="") -> ttk.Treeview: """ Treeviewとスクロールバーを持つframeを作成し、Notebookにaddする。 frameは、Treeviewとスクロールバーをセットする Treeviewは、listview形式、行は縞模様 Args: ttk.Notebook: ttk.Notebook string: tab_name Returns: Treeview: ツリービュー """ # tagを有効にするためstyleを更新 tkinter8.6?以降必要みたい # 表の文字色、背景色の設定に必要 self.style = ttk.Style() self.style.map('Treeview', foreground=self.fixed_map('foreground') , background=self.fixed_map('background')) # タブごとのスタイルの設定 self.style.configure(tab_name + ".Treeview") # frameの作成。frameにTreeviewとScrollbarを配置する frame1 = tk.Frame(parent, bg="cyan") # Treeviewの作成 treeview1 = ttk.Treeview(frame1, style=tab_name + ".Treeview") treeview1["show"] = "headings" # 表のデータ列だけを表示する指定 treeview1.tag_configure("odd", background="ivory2") # 奇数行の背景色を指定するtagを作成 # 水平スクロールバーの作成 h_scrollbar = tk.Scrollbar(frame1, orient=tk.HORIZONTAL, command=treeview1.xview) treeview1.configure(xscrollcommand=h_scrollbar.set) # 垂直スクロールバーの作成 v_scrollbar = tk.Scrollbar(frame1, orient=tk.VERTICAL, command=treeview1.yview) treeview1.configure(yscrollcommand=v_scrollbar.set) # pack expandがある方を後にpackしないと他が見えなくなる h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) # 先にパックしないと表示されない v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 先にパックしないと表示されない treeview1.pack(side=tk.TOP, fill=tk.BOTH, expand=True) parent.add(frame1, text=tab_name) return treeview1
◇タブごとのスタイル
読み込んだファイルごとに行の高さを一番高い行に合わせるために、タブ(実際には Treeview )ごとにスタイルを作成します。
タブの中には Treeview ウィジェットを作成します。
Treeview ウィジェットは行の高さをスタイルで指定 ( rowheight=
オプション)します。
ウィジェットのスタイルは同じクラスのウィジェットで共通です。
読み込んだファイルごとに行の高さが異なる場合、Notebook のタブに作成する Treeview ごとにスタイルを変える必要があります。
すべてのウィジェットにはデフォルトのスタイルがあります。
デフォルトのスタイル名はウィジェット名の前に「T」が付きます。
ただし、Treeview の場合は例外で Treeview のままです。
デフォルトのスタイルを元に派生した別のスタイルを指定することができます。
その場合、スタイル名を「新しい名前.古い名前」とします。
あるスタイルに基づく新しいスタイルの作成方法は、
- Styleのインスタンスを作成
- 「新しい名前.古い名前」形式の名前を使用してconfigure()メソッドを呼び出す
これを利用してタブ(実際には Treeview )ごとに異なる Treeview のスタイルを作成します。
スタイルの新しい名前は、タブ名にします。
つまり、「タブ名.Treeview」で新しいスタイルを定義します。
【コード】
self.style = ttk.Style() self.style.map('Treeview', foreground=self.fixed_map('foreground') , background=self.fixed_map('background')) # Treeviewの作成 treeview1 = ttk.Treeview(frame1, style=tab_name + ".Treeview") # Treeviewの行の高さを変更 # タブごとのスタイルの設定 self.style.configure(sheet_name1 + ".Treeview", rowheight = 18 * max_row_lines)
◆表の実装(Treeviewウィジェットの使い方)
Treeview ウィジェットはツリー形式や表形式でデータを表示するウィジェットです。
Treeview ウィジェットは tkinter.ttk パッケージに入っているので使用には import が必要です。
import tkinter.ttk as ttk
慣例的に ttk と別名を付けるようです。
表形式で表示する時の基本的な使い方です。
- インスタンス作成
- 配置 (pack)
- 表示形式の指定 (treeview["show"])
- 列の定義 (treeview.column)
- 見出しの設定 (treeview.heading)
- データ挿入 (treeview.insert)
この章では次の内容について説明します。
- 表(Listview)形式の指定
- 1行おきに背景色を設定する
- 列の定義
- 見出しの設定
- データ挿入追加:2021-12-10
- 列の幅の自動調整
- 行の高さの自動調整
- 列のソート追加:2022-07-22
◇表(Listview)形式の指定
Treeview はツリー形式で表示する部分と、表形式で表示する部分を持っています。
両方、どちらか片方だけの表示を選択できます。
デフォルトは両方を表示するので表形式だけを指定します。
指定は、インスタンスの show オプションか、Treeview のコンストラクタの show= 引数で行います。
self.tree["show"] = "headings" # listview形式の指定 # or ttk.Treeview(parent, show="headings")
オプション名 | 説明 | 設定値 |
---|---|---|
show | 表示対象を指定 | ・"tree":ツリー表示部分の表示 ・"headings":表形式部分の表示 ・["tree","headings"]:すべて(デフォルト) |
◇1行おきに背景色を設定する
行修飾を行うにはタグ (tag) を使用します。
タグはタグ名で区別して属性の異なるタグを複数設定できます。
行に対しても複数のタグを設定できます。
タグで使用できる主な属性 (オプション)
オプション名 | 説明 |
---|---|
background | 背景色 |
foreground | 前景色 |
font | フォント |
◎タグの使い方
タグ設定とデータ追加はどちらが先でも動作します。
- treeviewにタグ (tag) を設定
treeview.tab_configure(タグ名, オプション)
- タグを指定してデータ追加
データ追加時:treeview.insert(…, tags=タグ名)
◎背景色指定時のバグ
リンクのサイトにあるように Treeview の色付けにはバグがあるようです。
fixed_map メソッドを再定義して style の map メソッドを実行する必要があるようです。
【コード】
▼Treeview の色設定のバグフィクス
def fixed_map(self, option): # Fix for setting text colour for Tkinter 8.6.9 # From: https://core.tcl.tk/tk/info/509cafafae # # Returns the style map for 'option' with any styles starting with # ('!disabled', '!selected', ...) filtered out. # style.map() returns an empty list for missing options, so this # should be future-safe. return [elm for elm in self.style.map('Treeview', query_opt=option) if elm[:2] != ('!disabled', '!selected')]
# tagを有効にするためstyleを更新 tkinter8.6?以降必要みたい # 表の文字色、背景色の設定に必要 self.style = ttk.Style() self.style.map('Treeview', foreground=self.fixed_map('foreground') , background=self.fixed_map('background'))
▼タグの設定
# Treeviewの作成 treeview1 = ttk.Treeview(frame1, style=tab_name + ".Treeview") treeview1["show"] = "headings" # 表のデータ列だけを表示する指定 treeview1.tag_configure("odd", background="ivory2") # 奇数行の背景色を指定するtagを作成
▼タグを指定してデータ追加
# treeviewに要素追加。背景はtagを切り替えて設定 tree.delete(*tree.get_children()) # Treeviewをクリア for i, row in enumerate(rows): tags1 = [] # tag設定値の初期化 if i & 1: # 奇数か? i % 2 == 1: tags1.append("odd") # 奇数番目(treeviewは0始まりなので偶数行)だけ背景色を変える(oddタグを設定) tree.insert("", tk.END, values=row, tags=tags1) # Treeviewに1行分のデータを設定
◇列の定義
Treeview で列を操作するには、列の定義が必要です。
これは見出し名とは別で列操作のための識別子(カラム識別子)です。
指定は、インスタンスの columns オプションか、Treeview のコンストラクタの colums= 引数で行います。
treeview = Treeview(parent) treeview["columns"] = ["列1", "列2", "列3"] # or ttk.Treevew(parent, columns=["列1", "列2", "列3"])
実際のコードでは、CSV ファイルに列定義があるかどうかわからないので1で始まる整数を設定します。
【コード】
# 列定義の作成(現状は1開始の整数) columns_ = [i for i in range(1, ws.max_column + 1)] # 列定義を列数分行う。1スタート self.tree["columns"] = columns # treeviewの列定義を設定
◇見出しの設定
見出しの設定は、heading
メソッドで行います。
- 【構文】
treeview.heading("列1", text="列名")
列定義で指定した名前で列を指定して1列ずつ設定します。
合せて列の幅も見出しの文字の長さで設定します。
Font クラスの measure メソッドで文字列からピクセル値を取得し、
column
メソッドで width= 引数を指定して設定します。
【コード】
font1 = tkFont.Font() for col_name in columns: self.tree.heading(col_name, text=col_name) # 見出しの設定 width1 = font1.measure(col_name) + 10 # 見出しの文字幅をピクセルで取得 self.tree.column(col_name, width=width1) # 見出し幅の設定
◇データの挿入
データの挿入は、insert
メソッドで行います。
- 【構文】
treeview.insert(parent, index, iid=None, オプション)
- よく使う使い方
【構文】treeview.insert("", tk.END, value=データ列の値 (リスト))
- 引数
parent
:挿入するデータの親。""
で最上位index
:挿入位置。tk.END で最後尾に追加iid
:None
(省略も同じ)で iid の作成をお任せ
"I001" から順に割り当てる
※iid はアイテムごとのユニークな識別子
- オプション
values
:カラム順のカラムデータ(リスト)image
:画像(PhotoImage)オブジェクトを指定text
:ツリーカラムのテキストを指定tags
:タグを指定open
:子アイテムの展開を指定
【コード】
tree.insert("", tk.END, values=row, tags=tags1) # Treeviewに1行分のデータを設定
◇列の幅の自動調整
Treeview の列の幅は、列名で設定します。
- 【構文】
treeview.column(列名, width=幅)
列の幅は設定する文字の長さを計測します。
import tkinter.font as tkFont tkFont.Font().measure(文字列)
【処理】
見出しの文字幅で設定
- 見出しの個数分、以下を繰り返す
- 見出しの文字列の幅を計算
- 列幅として設定
- 見出しの個数分、以下を繰り返す
結果セットの文字幅で再設定
- 列の個数分、以下を繰り返す
- 同じ列のあるデータを文字に変換しその長さが最も長いデータを求める
max([x[i] for x in rows], key=lambda x:len(str(x)))
同じ列のデータをリストにして max 関数で長さを調べる - 文字列データの場合、改行を含む場合に改行で区切った中で長さが最も長いデータを求める
max(max_str.split("\n"), key=len)
- 求めたデータの文字幅を計算
font1.measure(max_str)
- 現在の列幅の設定値を取得
tree.column(tree['columns'][i], width=None)
width=None で呼び出す - 求めた文字幅が現在の列幅より大きい時は列幅を再設定
tree.column(tree['columns'][i], width=width1)
- 同じ列のあるデータを文字に変換しその長さが最も長いデータを求める
- 列の個数分、以下を繰り返す
【コード】
▼見出しに対して列の幅を設定
font1 = tkFont.Font() for col_name in columns: self.tree.heading(col_name, text=col_name) # 見出しの設定 width1 = font1.measure(col_name) + 10 # 見出しの文字幅をピクセルで取得 self.tree.column(col_name, width=width1) # 見出し幅の設定
▼データに対して列の幅を設定
font1 = tkFont.Font() # 要素の長さにより列幅を修正 for i, _ in enumerate(rows[0]): # 列数分回す(1行目の要素数分) # 同じ列のデータをリストにし列の値の長さを求め、最大となる列のデータを求める。 # 値は数字もあるので文字に変換し長さを求める。また、Noneは'None'となるので' 'とする。 max_str = max([x[i] for x in rows], key=lambda x:len(str(x))) or " " # 求めたものが文字列だったら、改行された状態での最大となるデータを求める。 # 厳密にはこの状態で最大となるデータを探さなければならないが割愛 if type(max_str) is str: max_str = max(max_str.split("\n"), key=len) width1 = font1.measure(max_str) + 10 # 文字幅をピクセルで取得 header1 = tree.column(tree['columns'][i], width=None) # 現在の幅を取得 # 設定済みの列幅より列データの幅の方が大きいなら列幅を再設定 if width1 > header1: tree.column(tree['columns'][i], width=width1) # 見出し幅の再設定
◇行の高さの自動調整
本来は、行ごとに高さの調整がしたかったのですが、そのやり方が分かりませんでした。
自分のなかでは、できないと判断しました。
仕方なく、ファイルに対しては同じ行の高さで、ただし、データが見えるように(行の高さを後から変更できないので)一番高い行に合わせて設定します。
ファイルによって一番高い行の高さは異なるので、ファイルごとに行の高さを変えます。
※ファイルごとに設定を変える方法は「タブごとのスタイル」を参照
考え方は簡単で、セルのデータが複数行の場合の行数を求めて、それを元に高さを設定します。
【処理】
列のデータを取得
[s for s in itertools.chain.from_iterable( self.dict_tables.get( sheet_name1)[0]) if type(s) is str]
辞書から値(rows, columnsのタプル)を取得、添え字0がrows
rows は openpyxl で取得した rows をリストに変換したもの
2次元のリストなので chain.from_iterable で平坦化してNoneを除いて取得対象がなければ処理を抜ける
抽出したリストの要素の中で改行の数の最も多い要素を取得
max(cells, key=lambda x:x.count("\n"))
max 関数でリストの中から改行文字「\n
」の数の最大値を取得
max 関数で引数を伴うメソッドで値を判断する場合 lambda 式を使用Treeview クラスのスタイルを設定
Treeview ごとのスタイルとして設定するため「ファイル名.Treeview」で設定
Treeview の行の高さは rowheight に設定
設定値は px 単位なので1行の高さを18pxとして計算
【コード】
# 一番行数の多い行に合わせて高さを設定する # 2次元のデータを平坦化しstr型だけを抽出する cells = [s for s in itertools.chain.from_iterable(self.dict_tables.get(tab_name1)[0]) if type(s) is str] if not cells: continue # 対象がない場合は抜ける # 抽出したリストの要素の中で改行の数の最も多い要素を取得 longest_cell = max(cells, key=lambda x:x.count("\n")) max_row_lines = longest_cell.count("\n") + 1 # 改行の数を数える # Treeviewの行の高さを変更 self.style.configure(tab_name1 + ".Treeview", rowheight = 18 * max_row_lines)
◇列のソート
別記事で紹介しています。
◆スクロールバーの設置
スクロールバーはスクロールバーウィジェットを作成してスクロールさせたいウィジェットと関連付けします。
◇コンストラクタ
- 【構文】
tk.Scrollbar(親ウィジェット, オプション)
よく使うオプションを使った時の構文
【構文】tk.Scrollbar(parent, orient=tk.HORIZONTAL, command=対象ウィジェット.xview)
【構文】tk.Scrollbar(parent, orient=tk.VERTICAL, command=対象ウィジェット.yview)
【主なオプション】
オプション 説明 設定値 orient スクロールの方向 HORIZONTAL:水平、VERTICAL:垂直 command スクロール対象ウィジェットの移動メソッド メソッド名 width スクロールバーの幅
(水平の場合は高さ、垂直の場合は幅)デフォルトは16
◇対象ウィジェットの割り付け
- 【構文】
対象ウィジェット.configure(xscrollcommand=横Scrollbarウィジェット.set)
- 【構文】
対象ウィジェット.configure(yscrollcommand=縦Scrollbarウィジェット.set)
【処理】
インスタンス作成
tk.Scrollbar(frame1, orient=tk.HORIZONTAL, command=treeview1.xview)
スクロールバーを対象ウィジェットに関連付け
treeview1.configure(xscrollcommand=h_scrollbar.set)
- xscrollcommand は水平スクロール発生時の呼び出しメソッドを指定
ここにスクロールバーの set メソッドを指定
※垂直方向の場合は x が y 、h が v
- xscrollcommand は水平スクロール発生時の呼び出しメソッドを指定
【コード】
# 水平スクロールバーの作成 h_scrollbar = tk.Scrollbar(frame1, orient=tk.HORIZONTAL, command=treeview1.xview) treeview1.configure(xscrollcommand=h_scrollbar.set) # 垂直スクロールバーの作成 v_scrollbar = tk.Scrollbar(frame1, orient=tk.VERTICAL, command=treeview1.yview) treeview1.configure(yscrollcommand=v_scrollbar.set) # pack expandがある方を後にpackしないと他が見えなくなる h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) # 先にパックしないと表示されない v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 先にパックしないと表示されない
◆ファイル選択ダイアログ
Tkinter にはファイル選択用のダイアログが用意されています。
tkinter.filedialog パッケージに入っているので使用には import が必要です。
from tkinter import filedialog
ダイアログを開いて選択された複数ファイル名を取得するには askopenfilenames メソッドを使用します。
【構文】
filedialog.askopenfilenames(オプション)
【オプション】
オプション 説明 設定値 parent 指定したウィンドウの上にダイアログを表示 ウィジェット title ダイアログのタイトルを指定 文字列 initialdir ダイアログを表示した時に開くディレクトリを指定 initialfile ダイアログを表示した時に選択されているファイル名を指定 filetypes 選択可能なファイルの種類を指定 (ラベル, パターン)のタプルのリスト(ワイルドカード可) multiple 複数ファイル選択の許可を指定 常に True:許可 【返り値】選択されたファイル名(文字列のタプル)、キャンセル時は空文字
更新:2024-03-25
◇filetypes
ファイルの種類の選択は、ワイルドカードが指定できますが、一般的なワイルドカードとは意味が異なるようです。
特別な指定として "*"
があり、すべてのファイルが対象です。
【構文】
[("表示名1", ".拡張子1 .拡張子2..."), ("表示名2", ".拡張子3 .拡張子4...")...]
【処理】
- オプションで開くファイルの拡張子を指定してファイルダイアログを開く
- 取得したファイル名をリストボックスウィジェットに対応付けている変数に設定
パス名だと長くなるのでファイル名だけにする
【コード】
def select_files(self, event=None): """ ファイル選択ダイアログを表示。選択したファイルパスを保存 """ # 拡張子の辞書からfiletypes用のデータを作成 # 辞書{".csv":("CSV", ","), ".tsv":("TSV", "\t")}、filetypes=[("CSV",".csv"), ("TSV",".tsv")] self.file_paths = filedialog.askopenfilenames(filetypes=[(value[0], key) for key, value in self.csv_op.extensions.items()]) basenames = [os.path.basename(file_path) for file_path in self.file_paths] self.var_csv_path.set(basenames) self.open_csv()
◆CSV ファイルの操作
CSV ファイルを扱うには、標準の CSV モジュールを使用します。
使用には import が必要です。
import csv
CSV ファイルを読み込んで使用する場合の基本は次の通りです。
この章では次の内容について説明します。
◇CSV ファイルのオープン
【構文】
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
【主な引数】
名前 説明 設定値 file 開くパス or ファイルのようなオブジェクト 文字列 or バイナリーモードで開いたファイルライクなオブジェクト mode ファイルが開かれるモード 'r':読み込み用に開く (デフォルト)
'w':書き込み用に開き、まずファイルを切り詰め
'x':排他的な生成に開き、ファイルが存在する場合は失敗
'a':書き込み用に開き、ファイルが存在する場合は末尾に追記newline ユニバーサル改行モード None, '', '\n', '\r', '\r\n'
のいずれか''
を推奨encoding 文字コード 例)cp932, utf_8
デフォルトはシステムのデフォルトエンコーディングでユニコード文字列
◇CSV ファイルの読み込み
【構文】
reader(csvfile, dialect='excel', **fmtparams)
戻り値は、reader クラスのインスタンスです。
【主な引数】
名前 説明 設定値 csvfile 開くパス or ファイルのようなオブジェクト 文字列 or バイナリーモードで開いたファイルライクなオブジェクト delimiter フィールド間を分割するのに用いられる 1 文字からなる文字列 デフォルトは ','
quotechar フィールドをクオートする際に用いられる 1 文字からなる文字列 デフォルトは '"'
quoting クオートがいつ reader によって認識されるかを制御 QUOTE_MINIMAL:特別な文字を含むフィールドだけをクオート(デフォルト)
QUOTE_NONNUMERIC:クオートされていない全てのフィールドを float 型に変換
QUOTE_NONE:クオート文字の特別扱いをしないskipinitialspace delimiter の直後に続く空白は無視するかどうか True:無視する、False:無視しない(デフォルト)
◇本アプリでの CSV ファイル読み込み
エンコーディングと区切り文字は、画面の選択肢とファイルの拡張子から指定して、 CSV ファイルをオープンして読み込みをします。
はじめに拡張子、ファイルダイアログでの表示名、区切り文字を共通で扱いたいので辞書を用意しました。
キーには、拡張子を当て、拡張子の判定に利用します。
【辞書の構造】 {拡張子:(ファイルダイアログでの表示名, 区切り文字)}
self.extensions = {".csv":("CSV", ","), ".tsv":("TSV", "\t"), ".txt":("Text", "\n")}
【処理】
- ファイルのパスから拡張子を取得
- 辞書から拡張子に対応した区切り文字を取得
- ファイルをオープン
エンコーディングをコンボボックスで選択したものに設定 - ファイルの読み込み
区切り文字を拡張子に合わせたものに設定 - 値をリスト化
- 全行で列数をそろえる
- 全行の最大列数を求める
- 不足している列を空文字で埋めてリストを再作成
【コード】
basename = os.path.basename(file_name) # 拡張子によってcsvのdelimiterを変える(辞書から取得) delimiter_ = self.extensions.get(os.path.splitext(file_name)[1])[1] with open(file_name, encoding=encode_, newline="") as csvfile: spamreader = csv.reader(csvfile, delimiter=delimiter_) rows1 = [row for row in spamreader] # 最大列数を求める column_len = max(len(v) for v in rows1) # 列の不足を空文字で補完する rows1 = [x + [""] * (column_len - len(x)) for x in rows1]
◆全体のソース
全体のソースはこちらから取得できます。
取得先:GitHub juu7g/Python-CSV-Viewer
◇バイナリ作成方法
pyinstaller
でバイナリ( exe
) ファイルを作成しました。
- 作成コマンド:
pyinstaller -F -w --additional-hooks-dir . ファイル名
※実行にはhook-tkinterdnd2.py
ファイルが必要です(作り方は「pyinstaller使用時の注意」を参照)
◆バイナリ(アプリ)
◇バイナリ取得先
バイナリも公開します。
こちらから取得してください。Github からダウンロード
◇使い方
インストール
ダウンロードした zip ファイルを任意のフォルダで解凍します
実行
- CSV_viewer.exe を実行します
- または CSV_viewer.exe のアイコンに表示したいファイルをドラッグアンドドロップします
操作
- ドラッグアンドドロップでの操作
- アプリ画面上の任意の位置に表示したいファイルをドラッグアンドドロップ
- ファイル選択での操作
- ファイル選択ボタンをクリックしファイルを選択
- 文字コードのエラーが出た場合
- 列の見出しをクリックしてソート
- ドラッグアンドドロップでの操作
画面の説明
- 指定したファイルの内容を表形式で表示します
- 複数ファイルを指定した場合、タブを変えて表示します
- ソートの時に見出しとしてソートしない行数を画面指定可能
アンインストール
解凍したファイルをすべて削除します
◇使用上の注意
制限事項があります。
- 制限事項
◆さいごに
ドラッグアンドドロップをやりたくて CSV Viewer を作ってみました。
少し調べると簡単そう(実際、とっかかりは簡単)なのですが、つまづくポイントもありました。
ドロップされたファイル情報の解析は、根気よくネット検索して何とか見つけることができました。
勉強という意味ではコードを書いて対応するのも良いのでしょうが、使えるものは使いたい派です。
同じように苦労したのが pyinstaller での exe 化です。
TkinterDnD2 のホームページに注意書きはあるのですが、良く理解できなくて。
英語だからではないような気もしました。
- SQLクライアントアプリの作り方(Tkinterで表)【Python】
- Excel viewerアプリの作り方(Tkinterでタブと表)【Python】
- 画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】
- シンプル画像ビューアの作り方(マウスホイール対応)【Python】
◇ご注意
本記事は次のバージョンの下で動作した内容を基に記述しています。
- Python 3.8.5
- TkinterDnD2 0.3.0
ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
更新:2022-07-22
◆更新情報
- 2022-07-22
- 列の見出しをクリックしてソートする機能を追加
◆参考
- TkinterDnD2解説:python tkinterでドラッグアンドドロップ - Emotion Explorer
- TkinterDnD2:tkinterdnd2 · PyPI
- TkinterDnD2仕様:Python: module TkinterDnD
- TkinterDnD2解説:emote-resize/resize.py at master · whuppo/emote-resize · GitHub
- TkDnD解説:TkDND Tutorial
- 自動判別:[Python][chardet] ファイルの文字コードの自動判別 - Qiita
- 配列補完:python - サイズの異なる行列の集合をパディングしてサイズを揃えたい - スタック・オーバーフロー
- Tkinter解説:Python:tkinter.(ttk.)Widget【ウィジェット関連】 - リファレンス メモ
- Tk解説:tk_getOpenFile manual page - Tk Built-In Commands