Python(tkinterの各種ウィジェットとクラス化)

Python(tkinterで〇進数変換アプリ)で紹介した2進数→10進数変換ツールのコードは、クラス化せずに、tkinterのrootを作成後、そのままラベルやエントリーなどのウィジェットを配置して作成していました。
今回はアプリケーション単位でフレームを作成するようにクラス化した方法で記述してみました。

import tkinter as tk

class App(tk.Frame):
    def __init__(self,master=None):
        super().__init__(master)
        self.master.title("2進数→10進数変換")
        self.master.geometry("250x100")
        self.create_widgets()        

    def create_widgets(self):
        tk.Label(self, text="2進数を入力してください").pack()
        self.entry = tk.Entry(self)
        self.entry.pack()
        btn=tk.Button(self,text="10進数に変換",command=self.convert)
        btn.pack()
        self.result_label = tk.Label(self,text="2進数を入力してボタンを押してください")
        self.result_label.pack()

    def convert(self):
        src=self.entry.get()
        try:
            self.result_label.config(text=f"変換結果は {eval('0b'+src)} です。")
        except:
            self.result_label.config(text="不正な形式です。")

if __name__ == '__main__':
    app = App()
    app.pack()
    app.mainloop()

実行結果は以前と変わりませんが、クラス化して1つのアプリケーションとしてまとめることで、コードも見やすくなり、機能別にクラス化することで複数の機能を有したアプリケーションが作りやすくなります。

沢山のウィジェットを有した画像表示アプリ

import re
import os
import os.path as P
import tkinter as Tk
from PIL import Image as I
from PIL import ImageTk as Itk
import tkinter.filedialog as D
import tkinter.messagebox as M

SIZE = 100
IMAGR_TYPES = ['gif', 'png', 'bmp', 'jpg', 'tif', 'ppm']
MAM_COLN = 6
GEO_MAIN = '+20+20'
GEO_SATE = '+750+50'
GEO_SCAL = '+750+400'
BGS = [('aliceblue', '#F0F8FF'), ('azure', '#F0FFFF'), ('beige', '#F5F5DC'),              \
       ('cornsilk', '#FFF8DC'), ('khaki', '#F0E68C'), ('lightgreen', '#90EE90'),          \
       ('lightpink', '#FFB6C1'), ('lightskyblue', '#87CEFA'), ('palegreen', '#98FB98')]

## functions ------------------------------------------------
def get_size(tup):
    """ It returns the size of images on the summary"""
    x, y = tup
    if (x<=100 and y<=100):
        return (x, y)
    elif x > y:
        r = float(SIZE) / float(x)
        return (100, int(y*r))
    else:
        r = float(SIZE) / float(y)
        return (int(x*r), 100)        

def make_regexp(types):
    """ It returns the regular expression of the image file type"""
    str = "\.("
    for k, v in types.items():
        if v.get():
            str += k + '|'

    str = str[0:-1] + ')$'
    return re.compile(str, re.I)

## classes ------------------------------------------------------------------
class ImageLabel(Tk.Frame):
    """ A Label class to show an image """
    # 一覧表示する画像のクラス.クリックされると原寸大のイメージを別窓で表示.

    id_original_size = None    # Label showing an original size image
    bg_var = None              # variable for background color

    def __init__(self, master, image_file, img):
        Tk.Frame.__init__(self, master)
        self.image = img # 一覧で表示する Tk.Image
        self.image_file = image_file # もとのイメージファイル
        img_label=Tk.Label(self, image=self.image)
        img_label.pack()
        txt_label=Tk.Label(self, text=P.basename(self.image_file), font=('Helvetica', '8'))
        txt_label.pack()
        img_label.bind('<Double-Button-1>', self.show)

    def show(self, event):
        label = ImageLabel.id_original_size
        if (label and label.winfo_exists()):
            top = label.winfo_toplevel()
            top.destroy()
        top = Tk.Toplevel(self)
        top.title(P.basename(self.image_file))
        top.geometry(GEO_SATE)
        img = I.open(P.abspath(self.image_file))
        self.timg = Itk.PhotoImage(img)
        label=Tk.Label(top, image=self.timg, bg=ImageLabel.bg_var.get())
        label.pack()
        ImageLabel.id_original_size = label

class ImageFrame:
    """ A Class to show summary of images """

    def __init__(self, master, **key):
        self.master=master
        self.key = key
        self.frame=Tk.Frame(master, **key)
        self.frame.pack(padx=5, pady=5)
        self.once = False

    def update(self, dir, types):
        if self.once:
            self.frame.destroy()
            self.frame=Tk.Frame(self.master, **self.key)
            self.frame.pack(padx=5, pady=5)
        self.once = True
        pat = make_regexp(types)
        files = [P.join(dir, file) for file in os.listdir(dir) if pat.search(file)]
        for i, file in enumerate(files):
            img_temp = I.open(file)
            img = img_temp.resize(get_size(img_temp.size), I.NEAREST)  
            tkimg = Itk.PhotoImage(img)
            la = ImageLabel(self.frame, file, tkimg)
            la.grid(row = int(i/MAM_COLN), column=int(i%MAM_COLN), sticky=Tk.S+Tk.W+Tk.E)

class Frame(Tk.Frame):
    """ The main class of this program. """

    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.master.title('Image Viewer')
        self.master.geometry(GEO_MAIN)
        self.cus_top = None # 背景色調節 window をあらわす内部変数

        f_dir = Tk.LabelFrame(self, text='Directory')
        f_dir.pack(anchor=Tk.W, padx=10, pady=2)
        self.e_dir = Tk.Entry(f_dir, width=50)
        self.e_dir.pack(side=Tk.LEFT)
        self.e_dir.bind('<Return>', self.show_sum)
        self.b_dir = Tk.Button(f_dir, text='Browse', command=self.browse)
        self.b_dir.pack(side=Tk.LEFT, padx=2)
        f=Tk.Frame(self)
        f.pack(fill=Tk.X)
        b_info=Tk.Button(f, bitmap='info', width=25, command=self.show_info)
        b_info.pack(side=Tk.RIGHT, padx=20, pady=10, anchor=Tk.CENTER)        
        f_type = Tk.LabelFrame(f, text='File Type')
        f_type.pack(side=Tk.LEFT, padx=10, pady=2)
        b_custom_bg = Tk.Button(f, text='Customize Background', command=self.cus_bg)
        b_custom_bg.pack(side=Tk.LEFT, anchor=Tk.S, padx=10)

        self.var_type = dict()
        for image_type in IMAGR_TYPES:
            self.var_type[image_type] = Tk.IntVar()
            cb = Tk.Checkbutton(f_type, text=image_type, variable=self.var_type[image_type],
                                relief=Tk.FLAT, justify = Tk.LEFT)
            cb.select()
            cb.pack(side=Tk.LEFT, padx=5)

        ImageLabel.bg_var = Tk.StringVar()
        ImageLabel.bg_var.set('#FFFFFF')

        self.f_bg = Tk.LabelFrame(self, text='Background')
        self.f_bg.pack(anchor=Tk.W, padx=10, pady=2)

        for name, code in BGS:
            rb = Tk.Radiobutton(self.f_bg, text=name, variable=ImageLabel.bg_var, value=code,
                                bg=code, command=self.change_bg, bd=1, relief=Tk.GROOVE)
            rb.pack(side=Tk.LEFT, padx=5)

        self.image_frame = ImageFrame(self, relief=Tk.RIDGE, border=2)

    def change_bg(self):
        bg1 = ImageLabel.bg_var.get()
        label = ImageLabel.id_original_size
        if label and label.winfo_exists():
            label.configure(bg=bg1)
            top = label.winfo_toplevel()
            top.focus_set()

    def browse(self):
        dir = D.askdirectory()
        if dir:
            self.e_dir.delete(0, Tk.END)
            self.e_dir.insert(0, dir)
            self.image_frame.update(dir, self.var_type)

    def show_sum(self, event):
        self.image_frame.update(self.e_dir.get(), self.var_type)        

    def show_info(self):
        M.showinfo(u"使い方",  u"Entry か Browse ボタンでディレクトリを選択してください。\n"
                               u"そのディレクトリに含まれる画像ファイルの一覧が表示されます。\n"
                               u"一覧にある画像をクリックすると、別窓でオリジナルサイズの画像が表示されます。\n"
                               u"透過型 GIF の場合は背景色を変えることができます。\n"
                               u"ラジオボタンにある背景色が気に入らない場合は、\n"
                               u"Customize Background ボタンを押して現れる\n赤、緑、青用の3つのスケールで"
                               u"背景色を調節してください。")

    def cus_bg(self):
        if not (self.cus_top and  self.cus_top.winfo_exists()):
            self.cus_top = Tk.Toplevel(self)
            self.cus_top.title('Create Back Ground')
            self.cus_top.geometry(GEO_SCAL)
            cusf = Tk.Frame(self.cus_top)
            cusf.pack(fill=Tk.BOTH, padx=10, pady=10)
            self.scale = dict()
            for i, color in enumerate(('red', 'green', 'blue')):
                l=Tk.Label(cusf, text=color+': ', anchor=Tk.W, fg=color, font=('Helvetica', '10', 'bold'))
                l.grid(row=i, column=0, sticky=Tk.W)
                self.scale[color] = Tk.Scale(cusf, orient=Tk.HORIZONTAL, length=300, from_=0, to=255, 
                                             command=self.customize_bg, tickinterval=50)
                self.scale[color].set(255)
                self.scale[color].grid(row=i, column=1)

            self.cus_top.bind('<FocusIn>', self.customize_bg)

    def customize_bg(self, event):
        ImageLabel.bg_var.set('#%02X%02X%02X' %
                        (self.scale['red'].get(), self.scale['green'].get(), self.scale['blue'].get()))
        self.change_bg()

##------------------------------------------------ 
if __name__ == '__main__':
    f = Frame()
    f.pack()
    f.mainloop()

このアプリケーションは、以下のサイトで紹介されていたものをPython3系に修正したものになります。
詳しい解説についても記載されていますので、各コードについて知りたい人はチェックしてみてください。

7. Widget てんこ盛り画像 Viewer

<2019/12/03追記>
今までの投稿で扱っていなかったウィジェットはCheckbuttonScaleSpinboxToplevelです。
また、tkinter.messageboxのようにダイアログを表示する方法で、 いろいろな質問ができるダイアログを作成するtkinter.dialogについて説明があります。

Menu,tkinter.scrolledtextを用いた画像表示アプリ

import re
import os
import os.path as P
import tkinter as Tk
from PIL import Image as I
from PIL import ImageTk as Itk
from PIL import ImageColor as Ic
import tkinter.scrolledtext as S
import tkinter.filedialog as D
import tkinter.messagebox as M

SIZE = 100
IMAGR_TYPES = ['gif', 'png', 'bmp', 'jpg', 'tif', 'ppm']
MAM_COLN = 6
GEO_MAIN = '680x600+20+20'
GEO_SATE = '+730+50'
GEO_SCAL = '+730+400'

## functions ------------------------------------------------
def get_size(tup):
    """ It returns the size of images on the summary"""
    x, y = tup
    if (x<=100 and y<=100):
        return (x, y)
    elif x > y:
        r = float(SIZE) / float(x)
        return (100, int(y*r))
    else:
        r = float(SIZE) / float(y)
        return (int(x*r), 100)        

def make_regexp(types):
    """ It returns the regular expression of the image file type"""
    str = "\.("
    for k, v in types.items():
        if v.get():
            str += k + '|'

    str = str[0:-1] + ')$'
    return re.compile(str, re.I)

## classes ------------------------------------------------------------------
class ImageLabel(Tk.Frame):
    """ A Label class to show an image """
    # 一覧表示する画像のクラス.クリックされると原寸大のイメージを別窓で表示.

    id_original_size = None    # Label showing an original size image
    bg_var = None              # variable for background color
    image_file_now = None

    def __init__(self, stxt, image_file, img):
        self.image = img # 一覧で表示する Tk.Image
        self.image_file = image_file # もとのイメージファイル
        frame = Tk.Frame(stxt, height=115, width=100)
        frame.pack_propagate(0)
        txt_label=Tk.Label(frame, text=P.basename(self.image_file), font=('Helvetica', '8'))
        txt_label.pack(side=Tk.BOTTOM)
        self.img_label=Tk.Label(frame, image=self.image)
        self.img_label.pack(side=Tk.BOTTOM)

        stxt.window_create(Tk.END, align=Tk.BASELINE, padx=5, pady=5, window=frame)
        self.img_label.bind('<Double-Button-1>', self.show)

    def show(self, event):
        label = ImageLabel.id_original_size
        if (label and label.winfo_exists()):
            top = label.winfo_toplevel()
            top.destroy()
        top = Tk.Toplevel(self.img_label)
        top.title(P.basename(self.image_file))
        top.geometry(GEO_SATE)
        img = I.open(P.abspath(self.image_file))
        self.timg = Itk.PhotoImage(img)
        label=Tk.Label(top, image=self.timg, bg=ImageLabel.bg_var.get())
        label.pack()
        ImageLabel.id_original_size = label
        ImageLabel.image_file_now = self.image_file

class Frame(Tk.Frame):
    """ The main class of this program. """

    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.master.title('BackGrounder')
        self.master.geometry(GEO_MAIN)
        self.cus_top = None # 背景色調節 window をあらわす内部変数
        self.fout = None

        ### Menu
        menu_bar = Tk.Menu(self, tearoff=0)
        # File
        menu_file = Tk.Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label="File", menu=menu_file,  underline=0)
        menu_file.add_command(label="Browse Dir.", command=self.browse, underline=0, accelerator = 'Ctrl-O')
        menu_file.add_command(label="ReLoad", command=self.load_dir, underline=0, accelerator = 'Ctrl-R')
        menu_file.add_command(label="Save As", command=self.save_image, underline=0, accelerator = 'Ctrl-S')
        menu_file_type = Tk.Menu(menu_file, tearoff=0)
        menu_file.add_cascade(label="File Type", menu=menu_file_type, underline=0)
        menu_file.add_separator()
        menu_file.add_command(label="Exit", command=self.exit, underline=0 , accelerator = 'Ctrl-Q')

        menu_bar.add_command(label="Back Ground", command=self.cus_bg, underline=0)
        menu_bar.add_command(label="Help", command=self.show_info, underline=0)

        # short-cuts
        self.master.bind('<Control-KeyPress-o>', self.browse)
        self.master.bind('<Control-KeyPress-s>', self.save_image)
        self.master.bind('<Control-KeyPress-r>', self.load_dir)
        self.master.bind('<Control-KeyPress-q>', self.exit)

        # check buttons in menu_file_type
        self.var_type = dict()
        for i, image_type in enumerate(IMAGR_TYPES):
            self.var_type[image_type] = Tk.IntVar()
            menu_file_type.add_checkbutton(label=image_type, variable=self.var_type[image_type])
            menu_file_type.invoke(i)

        ImageLabel.bg_var = Tk.StringVar()
        ImageLabel.bg_var.set('#FFFFFF')

        # add menu bar
        try:
            self.master.config(menu=menu_bar)     # this required to show the menu bar
        except AttributeError:
            self.master.Tk.call(master, "config", "-menu", menu_bar)

        self.once = False
        self.stxt = S.ScrolledText(self, bg=self.cget('bg'), cursor=self.cget('cursor'), state=Tk.DISABLED)
        self.stxt.pack(fill=Tk.BOTH, expand=1)
        self.pack(fill=Tk.BOTH, expand=1)

    def update(self, dir, types):
        self.stxt.configure(state=Tk.NORMAL)
        if self.once:
            self.stxt.delete('1.0', Tk.END)

        self.once=True
        pat = make_regexp(types)

        for f in os.listdir(dir):
            if pat.search(f):
                file = P.join(dir, f)
                img = I.open(file)
                ImageLabel(self.stxt, file,
                           Itk.PhotoImage(img.resize(get_size(img.size), I.NEAREST)))

        self.stxt.configure(state=Tk.DISABLED)

    def browse(self, event=None):
        self.dir = D.askdirectory()
        if self.dir:
            self.load_dir()

    def load_dir(self, event=None):
        self.update(self.dir, self.var_type)

    def show_info(self):
        M.showinfo(u"使い方",  u"メニューバーの File->Browse Dir. でディレクトリを選択してください。\n"
                               u"そのディレクトリに含まれる画像ファイルの一覧が表示されます。\n"
                               u"一覧にある画像をクリックすると、別窓でオリジナルサイズの画像が表示されます。\n\n"
                               u"透過型 GIF の場合は背景色をつけることができます。\n"
                               u"メニューバーの Back Ground をクリックすると\n"
                               u"赤、緑、青用の3つのスケールがある Window が現れるので、\n"
                               u"それで背景色を調節できます。\n\n"
                               u"背景色をつけた画像は File->SaveAs により保存することができます。"
                               )

    def cus_bg(self):
        if not (self.cus_top and  self.cus_top.winfo_exists()):
            self.cus_top = Tk.Toplevel(self)
            self.cus_top.title('Create Back Ground')
            self.cus_top.geometry(GEO_SCAL)
            cusf = Tk.Frame(self.cus_top)
            cusf.pack(fill=Tk.BOTH, padx=10, pady=10)
            self.scale = dict()
            for i, color in enumerate(('red', 'green', 'blue')):
                l=Tk.Label(cusf, text=color+': ', anchor=Tk.W, fg=color, font=('Helvetica', '10', 'bold'))
                l.grid(row=i, column=0, sticky=Tk.W)
                self.scale[color] = Tk.Scale(cusf, orient=Tk.HORIZONTAL, length=300, from_=0, to=255, 
                                             command=self.customize_bg, tickinterval=50)
                self.scale[color].set(255)
                self.scale[color].grid(row=i, column=1)

            #self.cus_top.bind('<FocusIn>', self.customize_bg)

    def customize_bg(self, event):
        ImageLabel.bg_var.set('#%02X%02X%02X' %
                        (self.scale['red'].get(), self.scale['green'].get(), self.scale['blue'].get()))
        self.change_bg()

    def change_bg(self):
        bg1 = ImageLabel.bg_var.get()
        label = ImageLabel.id_original_size
        if label and label.winfo_exists():
            label.configure(bg=bg1)
            top = label.winfo_toplevel()
            top.focus_set()

    def save_image(self, event=None):
        self.fout = D.asksaveasfilename(initialdir=self.dir, initialfile=self.fout and P.basename(self.fout) or None)
        if self.fout:
            img=I.open(ImageLabel.image_file_now)
            imgc = img.mode == 'RGB' and img or img.convert('RGB')
            bg1 = Ic.getrgb(ImageLabel.bg_var.get())         # background color
            ls=[]                                            # a sequence to store image data
            for c0 in imgc.getdata():
                if(c0==(255,255,255)):
                    ls.append(bg1)
                else:
                    ls.append(c0)

            imgc.putdata(ls)
            imgc.save(self.fout)

    def exit(self, event=None):
        self.master.destroy()

##------------------------------------------------ 
if __name__ == '__main__':
    f = Frame()
    f.pack()
    f.mainloop()

このアプリケーションは、以下のサイトで紹介されていたものをPython3系に修正したものになります。
詳しい解説についても記載されていますので、各コードについて知りたい人はチェックしてみてください。

8. Menu を使ったシンプル 画像 Viewer

今まで扱っていなかったウィジェットはMenuScrolledTextです。

Menuウィジェットについて詳しくはこちら
The Tkinter Menu Widget

また、ImageColorモジュールをインポートして、画像の背景色を変更したデータを元のデータに書き込んで保存できるようにしています。

Ic.getrgb(ImageLabel.bg_var.get())
pixel ごとの色の配列を返すメソッド
背景色のデータを読み込み、元の画像のpixcelが白部分は背景色を、白以外の部分は元のpixcelの色をリストとして保存するようにしています。
imgc.putdata(ls)
imgc に変換したデータ ls を putdata メソッドで書き込んでいます。
imgcは'RGB' and img or img.convert('RGB')で元のデータ形式がRGBであればそのままの形式で、RGBAなど異なる形式であればconvertメソッドでRGBに変換したデータです。
lsは背景色を書き込んだ画像のpixcelごとの色配列です。

ただし、α情報をもった画像データをRGB形式で保存する場合、αの情報がなくなるため、コンソールには以下のような警告表示がされます。


これは元のThe Python Imaging Libraryで定義されている仕様のようです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください