Ccmmutty logo
Commutty IT
15 min read

自分の欲しいを全部乗せ!tkinterでメモ帳作り「3.5_既存の複数ファイルをドラッグアンドドロップでまとめて開く」

https://cdn.magicode.io/media/notebox/ee33376f-7b53-4545-a500-859bfb0aa2a3.jpeg

はじめに

このシリーズでは、tkinterを使って自分が欲しいと思う機能を全部乗せた 「自分だけのメモ帳」 を作成していきます。
一連の目次については、この記事の一番下に記載しています。
 
このページでは、
「既存のファイルをドラッグアンドドロップで開く」機能を追加します。
ドラッグアンドドロップは「複数ファイルをまとめて」実施できるようにします。

最初に作成したものを確認する

ドラッグアンドドロップでファイルを開く

ドラッグアンドドロップでファイルを開く流れは、前回実施した 3.4_複数ファイルをまとめて開く とだいたい同じで
  1. 複数ファイルをテキストエリアにドラッグアンドドロップする
  2. ドラッグアンドドロップしたファイルの数だけループしサブプロセスでメモ帳を起動する
  3. 対象ファイルの内容を読み込んで、テキストエリアに表示する
という感じです。
 

今回作成したコード

TkinterDnD2 で rootウィンドウを生成する

ウィンドウ自体にドラッグアンドドロップの機能を持たせます。
そのため、rootウィンドウの生成に「TkinterDnD2」というモジュールを使用します。
 
TkinterDnD2 については下記ページがとても分かりやすかったです。
ありがとうございます!
 
【Python】tkinter:ファイルのドラッグアンドドロップ(パス取得、画像表示) | OFFICE54 https://office54.net/python/tkinter/file-drag-drop
 
root = tkinter.Tk()
↓ドラッグアンドドロップでウィンドウを生成できるように変更した
root = TkinterDnD.Tk()

追加した部分_モジュールのインポート

############
# モジュールインポート

## D&Dを実現するためのライブラリ # pip install tkinterdnd2
from tkinterdnd2 import *

## 複数文字で区切ったりするため、正規表現を使用する 標準ライブラリ
import re
ドラッグアンドドロップでファイルを開くために、新たに2つのモジュールをインポートしています。

tkinterdnd2

ドラッグアンドドロップでファイルパスを取得できるようにします。

re

pythonで正規表現を扱うためのモジュールです。検索するre.searchや置換のre.subなど、文字列を色々扱うときに頻繁に使用します。
今回は文字列を指定の文字で分割する re.split を使用します。
 
reモジュールについては下記ページがとても参考になります。
いつもありがとうございます!!
 
Pythonの正規表現モジュールreの使い方(match、search、subなど) | note.nkmk.mehttps://note.nkmk.me/python-re-match-search-findall-etc/
 
reは標準モジュールです。pipインストールなどは不要です。
 

追加した部分_動作を提供する関数

前回作成した既存ファイルを開く関数「openFile()」にD&D用の内容を追加します。

元の関数

###############################
#### 各種機能を提供する関数 ####
###############################

#####################
# ウィンドウ関連

## 既存のファイルを開く
def openFile(self):
    global file
    FILENAME_tuple = tkinter.filedialog.askopenfilename(filetypes=[("All Files","*.*")],multiple=True)
    for FILENAME in FILENAME_tuple:
        openFile_subprocess(FILENAME)

↓ Ctrl+o と D&D で処理を分岐する

## 既存のファイルを開く
def openFile(self):
    global file
    # 渡された self について
    # bindで呼び出した場合、print(type(self)) →<class 'tkinter.Event'> # print(self) →<KeyPress event state=Control keysym=n keycode=78 char='\x0e' x=265 y=-965>
    # dndで開いた場合 →<class 'tkinterdnd2.TkinterDnD.DnDEvent'>

    if isinstance(self,tkinter.Event):
        FILENAME_tuple = tkinter.filedialog.askopenfilename(filetypes=[("All Files","*.*")],multiple=True)
        for FILENAME in FILENAME_tuple:
            openFile_subprocess(FILENAME)

    elif isinstance(self,TkinterDnD.DnDEvent):
        # DnDで開いた場合、selfにはD&Dされたファイルのファイルパスがリストで入っている
        # ic| self.data: ('{C:/フルパス/テストファイル0.txt} '
        #         '{C:/フルパス/テストファイル1.txt} '
        #         '{C:/フルパス/テストファイル2.txt}')

        for i, FILENAME in enumerate(re.split('[{}]',self.data)):
            FILENAME=FILENAME.lstrip("{").rstrip("}")
            if len(FILENAME) <= 1:
                continue
            openFile_subprocess(FILENAME)

bindとDnDの処理分岐について

コメントに記載していますが、
Ctrl+oのbindで開いたときと、DnDで開いた時で、渡される self が異なります。
この内容によって処理を分岐します。
型の判定に isinstance() を使用します。
Pythonで型を取得・判定するtype関数, isinstance関数 | note.nkmk.mehttps://note.nkmk.me/python-type-isinstance/

DnDで開いた場合のファイルパス処理について

DnDで開いた場合、渡される引数selfにはD&Dされたファイルのファイルパスがリストで入っています。
中括弧などを取り除きつつ、forループで1ファイルずつ処理します。

追加した部分_テキストエリアにD&Dできるようにする

#################
# ドラッグアンドドロップでファイルを開く処理関連

## テキストアリアにファイルをドラッグアンドドロップできるようにする
TextArea.drop_target_register(DND_FILES)

## ドラッグアンドドロップしたときの呼び出す関数を紐付ける
TextArea.dnd_bind('<<Drop>>', openFile)
D&D機能を追加し、openFile関数と紐付けます。

メモ帳コード全体

############
# モジュールインポート # Pythonのモジュールとimportとfrom入門 - Qiita https://qiita.com/niwaka_dev/items/6e3d9ff6d797243c77c3

## tkinter モジュールをインポートする 標準ライブラリ
import tkinter
## D&Dを実現するためのライブラリ # pip install tkinterdnd2
from tkinterdnd2 import *

## ファイル操作関連
### tkinter でファイルダイアログ操作する 標準ライブラリ
import tkinter.filedialog
### 文字コードを自動識別するライブラリ # pip install chardet
import chardet
### ファイルの名前やパスを取得するときに使用している 標準ライブラリ
import os

## 別ウィンドウを開くときに使用している2つ 標準ライブラリ
import subprocess
import sys

## 複数文字で区切ったりするため、正規表現を使用する 標準ライブラリ
import re

## debug用 # pip install icecream
from icecream import ic

############
# ウィンドウの基本設定

## 実行時の引数を取得する
args =sys.argv # print(args[0]) # →C:\Users\フルパス\stickypynote.py

## rootの設定
root = TkinterDnD.Tk()
root.title("Untitled.txt") # タイトルバーに表示される文字列を指定する

x, y=644,188 # 最初のウィンドウサイズを指定する
root.geometry('%dx%d' % (x, y))

## テキスト入力エリア TextArea を作成
TextArea = tkinter.Text(root, font="メイリオ 8",wrap=tkinter.CHAR,undo=True,maxundo=0)
### wrap=tkinter.CHAR 文字単位で折り返す
### undo=True,maxundo=0 undoを有効にして、何回でもundoできる(0以下で無限)

###############
# スクロールバーの設定
## テキストエリアウィジェットに紐付ける形でスクロールバーを作成する
Scroll = tkinter.Scrollbar(TextArea)
## 右側に配置する。テキストエリア内で空きスペースが出来ないように、縦横に拡げて配置する
Scroll.pack(side=tkinter.RIGHT,  fill=tkinter.BOTH)
## スクロールバーのドラッグで縦軸方向にスクロールできるようにする
Scroll.config(command=TextArea.yview)
## テキストエリアウィジェットにスクロールバーをセットする
TextArea.config(yscrollcommand=Scroll.set)

###############################
#### 各種機能を提供する関数 ####
###############################

#####################
# 最前面on/off

# 最前面に表示する関数、最前面ボタンも切り替える
def saizen_on():
    root.attributes('-topmost',True), # ic(root.attributes('-topmost')) # ic| root.attributes('-topmost'): 1
    button_top_on.grid_remove(),
    button_top_off.grid()

# 最前面表示を解除する関数、最前面ボタンも切り替える
def saizen_off():
    root.attributes('-topmost',False), # ic(root.attributes('-topmost')) # ic| root.attributes('-topmost'): 0
    button_top_off.grid_remove(),
    button_top_on.grid()

# キーバインドショートカットから呼び出す
def saizen_on_bind(self):
    press_key=self.keysym # print(self) # <KeyPress event state=Control|0x40000 keysym=Right keycode=39 x=415 y=76>
    if press_key == "Right":
        saizen_on()
    elif press_key == "Left":
        saizen_off()

#####################
# ウィンドウ関連

## 新規ウィンドウで新規作成する
def newFile(self):
    subprocess.Popen(('python "'+args[0]+'"'), stdout = subprocess.PIPE, shell=True)

## ウィンドウを閉じる
def quitApp(self):
    root.destroy()

## ファイルを開くときに実施する共通の部分
def openFile_commonProcess(FILENAME):
    tmp = open(FILENAME,"rb")
    encode = chardet.detect(tmp.read())["encoding"]
    tmp.close()
    file = open(FILENAME,"r",encoding=encode,errors="ignore")
    text = file.read()
    file.close()
    #一度文字を全て削除してから挿入する
    TextArea.delete("1.0",tkinter.END)
    TextArea.insert("1.0",text)
    #FILENAMEにはフルパスが入っているため、タイトルだけ取得
    file_title=os.path.basename(FILENAME)
    root.title(file_title)

## サブプロセスで他のファイルを開く
def openFile_subprocess(FILENAME):
    jikko='python "'+args[0]+'" '+FILENAME
    subprocess.Popen((jikko), stdout = subprocess.PIPE, shell=True)

## 既存のファイルを開く
def openFile(self):
    global file
    # 渡された self について
    # bindで呼び出した場合、print(type(self)) →<class 'tkinter.Event'> # print(self) →<KeyPress event state=Control keysym=n keycode=78 char='\x0e' x=265 y=-965>
    # dndで開いた場合 →<class 'tkinterdnd2.TkinterDnD.DnDEvent'>

    if isinstance(self,tkinter.Event):
        FILENAME_tuple = tkinter.filedialog.askopenfilename(filetypes=[("All Files","*.*")],multiple=True)
        for FILENAME in FILENAME_tuple:
            openFile_subprocess(FILENAME)

    elif isinstance(self,TkinterDnD.DnDEvent):
        # DnDで開いた場合、selfにはD&Dされたファイルのファイルパスがリストで入っている
        # ic| self.data: ('{C:/フルパス/テストファイル0.txt} '
        #         '{C:/フルパス/テストファイル1.txt} '
        #         '{C:/フルパス/テストファイル2.txt}')

        for i, FILENAME in enumerate(re.split('[{}]',self.data)):
            FILENAME=FILENAME.lstrip("{").rstrip("}")
            if len(FILENAME) <= 1:
                continue
            openFile_subprocess(FILENAME)

#####################
# 各ウィジェットの設定
#
# 参考
# PythonのTkinterでGUIアプリを作る - Qiita # https://qiita.com/canard0328/items/5ea096352e160b8ececa

## 最前面on/offボタン
### 最前面表示をoffにする関数を呼ぶボタン
button_top_off = tkinter.Button(root, font="meiryo 4",text = "▲",command=saizen_off)
### 最前面表示をonにする関数を呼ぶボタン
button_top_on = tkinter.Button(root,font="meiryo 4", text = "▽",command=saizen_on)

############
# gridの設定

## 最前面on/offボタン
button_top_off.grid(row=0,column=1,sticky=tkinter.E)
button_top_on.grid(row=0,column=1,sticky=tkinter.E)

## テキスト入力エリア
TextArea.grid(row=1,column=0,columnspan=2,sticky=tkinter.NSEW)

# 作成したウィンドウについて、各行列をどのように伸縮するか
root.rowconfigure(1, weight=1)
root.columnconfigure(0, weight=1)

#################
# キーバインドでショートカット実行できるようにする

## 最前面のon/offを切り替えるショートカット
root.bind_all("<Control-KeyPress-Right>", saizen_on_bind)
root.bind_all("<Control-KeyPress-Left>", saizen_on_bind)

## ウィンドウ関連
### 新規ウィンドウで新規作成する
root.bind_all("<Control-KeyPress-n>", newFile)
### ウィンドウを閉じる
root.bind_all("<Control-KeyPress-q>", quitApp)
root.bind_all("<Control-KeyPress-w>", quitApp)
### 既存のファイルを開く
root.bind_all('<Control-KeyPress-o>', openFile)

#################
# ドラッグアンドドロップでファイルを開く処理関連

## テキストアリアにファイルをドラッグアンドドロップできるようにする
TextArea.drop_target_register(DND_FILES)

## ドラッグアンドドロップしたときの呼び出す関数を紐付ける
TextArea.dnd_bind('<<Drop>>', openFile)

## 起動時の引数が1より大きかった場合、つまり既存ファイルを指定して起動した場合の処理
if len(args) > 1:
    FILENAME = ' '.join(args[1:])
    openFile_commonProcess(FILENAME)

#################
# メインループ
root.mainloop()
 

おわりに。簡単に大量のメモが開ける!

今回は既存のテキストファイルを、ドラッグアンドドロップで開く機能を追加しました。
前回のファイルダイアログで開くよりも、簡単にファイルオープンできます!
 
私はこの「自分のメモ帳」用のテキストファイルを1つのフォルダにまとめていて、
全選択→ドラッグアンドドロップ
でまとめてファイルを開いています。
1つ1つ確認しながら開くよりも気軽でよいですね!
 

Discussion

コメントにはログインが必要です。