Magicode logo
Magicode
16 min read

自分の欲しいを全部乗せ!tkinterでメモ帳作り「3.6_ファイルを保存する」

https://cdn.magicode.io/media/notebox/3b83c6aa-f461-429e-8257-316feffa7f41.jpeg

はじめに

このシリーズでは、tkinterを使って自分が欲しいと思う機能を全部乗せた 「自分だけのメモ帳」 を作成していきます。
一連の目次については、この記事の一番下に記載しています。
 
このページでは、
「ファイルを保存する」機能を追加します。

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

ファイルを保存する

ファイルを保存する流れは
  1. 新規ファイルの保存か、上書き保存かで処理を分岐する
  2. 新規ファイルの場合は
    1. 保存ダイアログを表示する
    2. 入力された内容を読み込んで保存する
    3. タイトル表示を更新する
  3. 既存ファイルの上書き保存の場合は
    1. 既存のファイルパスのファイルに対して
    2. テキストエリアの情報を読み込んで保存する
という感じです。
 
ファイル保存機能の呼び出しには
今回もキーボードショートカットを使用します。
 

設定しているキーボードショートカットについて

操作内容ショートカット
新規ウィンドウを作成するCtrl + n
ウィンドウを閉じるCtrl+w, Ctrl+q
既存のファイルを開くCtrl+o
【NEW】ファイルを保存するCtrl+s
ウィンドウを常に最前面表示するCtrl + →
ウィンドウの最前面表示を解除するCtrl + ←
「Ctrl と sキー」を組み合わせて、
ファイルを保存します。
 

今回作成したコード

追加した部分_入力と関数をbindする

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

## ウィンドウ関連

### ファイルを保存する
root.bind_all("<Control-KeyPress-s>", saveFile)
 
"<Control-組み合わせる入力>" で Ctrl+入力 というようにショートカット操作が設定できます。
「KeyPress-」でキーボードの入力したときをトリガーにする、
「s」が入力するキーです。

追加した部分_使用するモジュールの確認

今回は追加のモジュールはありません。
ファイルを開くときにインポートした「tkinter.filedialog」の「asksaveasfilename」を使用します。

tkinter.filedialog

tkinterでファイル保存ダイアログを開く場合は、tkinter.filedialogモジュール の asksaveasfilename を使用します。
下記がオプションなどわかりやすかったです、ありがとうございます!
【Python/tkinter】名前を付けて保存ダイアログボックスの表示 | イメージングソリューション https://imagingsolution.net/program/python/tkinter/asksaveasfilename/
 

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

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

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

## ファイルを保存する
def saveFile(self):
    global file
    # 新規ファイルのセーブか既存ファイルのセーブかで分岐
    # fileに文字列がない、つまり新規ファイルの場合
    if file == None:
        file = tkinter.filedialog.asksaveasfilename(initialfile = 'Untitled'+'.txt', defaultextension=".txt",
                           filetypes=[("Text Documents", "*.txt")])
        # fileに入力がない、つまりセーブしてない場合
        if file =="":
            file = None
            # セーブせずにEscなどで戻った場合文字列"nosaved"を返す
            return "nosaved"

        else:
            # ファイル名が指定されていた場合は保存する
            f = open(file, "w",encoding="UTF-8",errors="ignore")
            f.write(TextArea.get("1.0", "end-1c"))
            f.close()
            root.title(os.path.basename(file))

    else:
        # 既にfileに文字列がある、つまり既存ファイルの上書きの場合
        # 新規で作成した場合と既存のファイルを開いた場合でfileに入る文字列が異なるため揃える
        if type(file) != str :
            file = file.name
        # ファイルのパスを取得
        file_path=os.path.dirname(file)
        # ファイルに書き込んでクローズ
        f = open(file, "w",encoding="UTF-8",errors="ignore")
        f.write(TextArea.get("1.0", "end-1c"))
        f.close()
引っかかりポイントとして
# 既にfileに文字列がある、つまり既存ファイルの上書きの場合
        # 新規で作成した場合と既存のファイルを開いた場合でfileに入る文字列が異なるため揃える
        if type(file) != str :
            file = file.name
新規ファイルから保存したファイルをさらに上書きする場合と
既存ファイルを開いて、ファイルを上書きする場合で
ファイルパスを指定している file 変数に入る情報が異なります。
typeで確認してfileの中身を揃えて保存処理しました。
# fileに入力がない、つまりセーブしてない場合
        if file =="":
            file = None
            # セーブせずにEscなどで戻った場合文字列"nosaved"を返す
            return "nosaved"
今後、他の処理からこのsaveFile関数を呼ぶ機能を追加します。
その際、ちゃんと保存したかどうかを確認するための処理を追加しておきます。

メモ帳コード全体

############
# モジュールインポート # 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以下で無限)

## ファイルパスを初期化しておく
file = None

###############
# スクロールバーの設定
## テキストエリアウィジェットに紐付ける形でスクロールバーを作成する
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):
    global file
    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)

## ファイルを保存する
def saveFile(self):
    global file
    # 新規ファイルのセーブか既存ファイルのセーブかで分岐
    # fileに文字列がない、つまり新規ファイルの場合
    if file == None:
        file = tkinter.filedialog.asksaveasfilename(initialfile = 'Untitled'+'.txt', defaultextension=".txt",
                           filetypes=[("Text Documents", "*.txt")])
        # fileに入力がない、つまりセーブしてない場合
        if file =="":
            file = None
            # セーブせずにEscなどで戻った場合文字列"nosaved"を返す
            return "nosaved"

        else:
            # ファイル名が指定されていた場合は保存する
            f = open(file, "w",encoding="UTF-8",errors="ignore")
            f.write(TextArea.get("1.0", "end-1c"))
            f.close()
            root.title(os.path.basename(file))

    else:
        # 既にfileに文字列がある、つまり既存ファイルの上書きの場合
        # 新規で作成した場合と既存のファイルを開いた場合でfileに入る文字列が異なるため揃える
        if type(file) != str :
            file = file.name
        # ファイルのパスを取得
        file_path=os.path.dirname(file)
        # ファイルに書き込んでクローズ
        f = open(file, "w",encoding="UTF-8",errors="ignore")
        f.write(TextArea.get("1.0", "end-1c"))
        f.close()

#####################
# 各ウィジェットの設定
#
# 参考
# 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)
### ファイルを保存する
root.bind_all("<Control-KeyPress-s>", saveFile)

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

## テキストアリアにファイルをドラッグアンドドロップできるようにする
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()
 

おわりに。書き捨てからの解放!

今回はファイルを保存する機能を追加しました。
やっと書き捨てずに残すことが出来ますね!
3章では、
  • 新規ウィンドウを開く
  • 既存のファイルを開く
  • 内容を保存する
という、メモ帳に必要な最低限の機能を揃えてきました。
これで実際に使い始めることができそうです…!
メモ帳の必要最低限の機能を持たせるだけでも、色々と新しい発見がありました。
次回からは、さらに自分が欲しいと思う独自の機能をドンドン乗せて「自分だけのメモ帳」にしていけたらと思います!
 
今回も最後までお読みいただきありがとうございました!
いつも「いいね♥」もありがとうございます!
この記事を読んで
「普通のメモ帳ってすごいな」
「自分のお気に入りのメモ帳の仕組みも気になってきた!」
と思っていただけたら、左部|上部 にあるハートボタンでいいね頂けると嬉しいです♪
 
また「こんな機能があるとメモ帳がさらに便利になるよ」「あのメモ帳のあの機能が神だ」などあればコメントやご連絡いただけると嬉しいです!
 

Discussion

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