LoginSignup
6
12

More than 3 years have passed since last update.

Python/Tkinter 試行錯誤リファレンス

Last updated at Posted at 2020-05-30

はじめに...

タイトル長くてスミマセン!!!!!
書きたいことの要約って難しいですよね...
誰か、要約案を私にお恵みください...
(ちなみに、この後も長いです。目次をご活用ください。また、本編(?)は"ここからスタート!"から始まります。どうぞご自由にスキップしてください。)
あと、執筆途中であることに十分ご留意ください。

(あと、当然っちゃ当然ですが、GUIが使える環境じゃないと動きません。)

Python/Tkinterとは?

簡単に言うと、Tclというスクリプト言語があってですね、
その中にGUIをいじくれるTkっつうのがありまして、
で、その言語は、Python内部に組み込まれておりまして(ソースからビルドとかしない限り)、
そいつを利用して、GUIを司れるってわけです。
(まあ、ついでに言うと、そのTclってのを直接使うこともできます。)

Tkinterリファレンス

ここからスタート!

tkinterインストール

Windowsの場合スキップしていただいて構いません。
(Pythonのインストール時に標準設定でインストールするか、"tcl/tk and IDLEをオンにしてあればの話")
まあ、どうしてもやりたければ、頑張ってビルドしてもいい気もしますが、ようわかりません。

Ubuntu&Debian

aptが使えることを願います...

sudo apt update #共通(パッケージリストの更新...念のため...)
sudo apt install python-tk #python2
sudo apt install python3-tk #python3

その他もろもろLinux(パッケージマネージャ)

...残念ながら、勉強不足でわからんのでG先生にお尋ねください(丸投げ)

その他もろもろLinux(ソースから)

まず、ここからソースコードを持ってきます。
tclもtkもです(同じバージョンで)。
Pythonのもここから持ってきます。バージョンは...お好みで。
その後、解凍します。で、

mkdir build
mkdir build/tk
mkdir build/tcl
mkdir build/python

cd [解凍して出てきたフォルダ名(tcl)]/unix/
./configure --prefix=[上で作ったbuild/tclの絶対パス]
make
sudo make install
cd ../../

cd [解凍して出てきたフォルダ名(tk)]/unix/
./configure --prefix=[上で作ったbuild/tkの絶対パス] --with-tcl=[[解凍して出てきたフォルダ名(tcl)]/unix/の絶対パス]
make
sudo make install
cd ../../

cd [解凍して出てきたフォルダ名(python)]
./configure --with-tcltk-libs="-L[library path]" --with-tcltk-include="-I[include path]"
make
sudo make install
cd ../../

MacOS

インストーラーもあるのですが、それを使ってもIDLEに日本語書けないとかいろいろあるようなので、
homebrewを使ってインストールor ActiveStateから、ActiveTclをインストール&Pythonビルド(一向にビルドに成功しない) しましょう...
または、Python3.10は修正された模様なので、そちらを使いましょう。

tkinterインポート!

#Python2なら...
import Tkinter #tkinter 本体
import ttk #tkinter 拡張(?)
#Python3なら...
import tkinter #tkinter 本体
from tkinter import ttk #tkinter 拡張(?)

Python2と3でモジュール名が違うので注意。
ちなみに、次のように書くと2,3両対応できる。

try:
    import Tkinter as tkinter
    import ttk
except:
    import tkinter
    from tkinter import ttk

ちなみに、先程からちょこちょこ登場している"ttk"はtkinterの拡張モジュール(?)で、
これを使うと、スタイル(テーマ)を変えることができる。

ここで以下のようなエラーが出る場合、

ImportError: No module named '_tkinter’

_tkinter即ち、tkinterのコア部分がない言ってエラーなので、
ソースをビルドしているなら、"--with-tcltk-includes='-L[パス]' --with-tcltk-libs='-L[パス]'"
を正しく指定していることを確認してください。

ウィンドウ

メインウィンドウ

次に、メインウィンドウを作る。

root = tkinter.Tk()#この後をよく読むこと

ただし、このままだとUbuntu等のOSでタスクバーのアプリケーション名が"Tk"と表示されダサいので、

root = tkinter.Tk(className="[アプリ名]")

とする。

必ずプログラムの最後には、

root.mainloop()

と書くこと。

(ちなみにこれを書かないとプログラムが終了します。)

サブウィンドウ

sub = tkinter.TopLevel()

とする。
このサブウィンドウはメインウィンドウの設定を引き継ぎます。
sub.mainloop()は要りません。

全画面表示

root.attributes("-fullscreen", True)

戻すときは、

root.attributes("-fullscreen", False)

アイコン

このアイコンは、タスクバーにも表示されます。
.icoファイルの場合、

root.iconbitmap('[イメージへのパス(相対でも絶対でもどっちでもいい)]')

pngを使うときは(icoも使える模様)

root.iconphoto(True, tkinter.PhotoImage(file='[イメージへのパス(相対でも絶対でもどっちでもいい)]'))

この文の中のTrueはサブウィンドウにもここで設定したアイコンを適用するかどうかのフラグで、Falseにするとサブウィンドウには適用されなくなります。

(なお、tk.callでイメージを表示するというのはなんだか面倒なので割愛します。)

ウィジェット

ボタン

def callback():
    print("Hello!!")

button = ttk.Button(root, text="[表示するテキスト]",command=callback)

commandには、ボタンが押されたときに実行する関数の名前を指定する。
もし、引数を指定したい場合は、

def callback(hogehoge):
    print(hogehoge)

button = ttk.Button(root, text="[表示するテキスト]",command=lambda: callback("Hello!!!"))

フレーム(枠)

frame = ttk.Frame(root)

えーっと... frameはウィンドウと他のウィジェットのrootの部分に入れて使います。
なんなら、色やら形を変えることもできます。(後で追記します。)
下に活用例を示します。

def callback(hogehoge):
    print(hogehoge)

frame = ttk.Frame(root)
button = ttk.Button(frame, text="[表示するテキスト]",command=lambda: callback("Hello!!!"))

ラベル(一行の選択できないテキスト)

label = ttk.Label(root, text="[表示するテキスト]")

ウィジェットの配置

tkinterではウィジェットを定義したあとに配置する必要があります。
配置方法は3つあります。

詰め込み方式(pack)

詰め込み方式(pack)では、ウィンドウの空いているところにウィジェットをツメコミマス。(語彙力...)

[ウィジェット].pack([オプション])

[ウィジェット]はウィジェットのインスタンス、
(インスタンスやら関数などがワケワカメな人はぜひ他のPython入門記事をご覧ください。)
[オプション]には次が指定できます(何も指定しなくても問題ない。):
・side
 どの方向に配置するか設定できます。
 設定可能値:
  "right"右,"left"左,"bottom"下,"top"上
・fill
 どの方向に対して埋め立てるか設定できます。
 設定可能値:
  "both"-両方向
  "x"-x方向(縦?)
  "y"-y方向(横?)
  (正直、不安なので実験して教えていただけると幸いです。)
・anchor
 どこに配置するか設定できます。(sideに似ていますが、より具体的)
 設定可能値:
  えーっと...こんな感じです。(ここより引用)
  nw --- n --- ne
  |      |
  w  c   e
  |      |
  sw --- s --- se
・expand
 空白が生まれたとき(ウィンドウのサイズが変わったときなど)に埋めるかどうか設定できます
 設定可能値:
  True or False
  (面倒臭さが表立っているのは気のせいですからね...本当ですよ......)

ドラッグ&ドロップ

tkinterでは、tkdndとTkinterDnD2を使うことでファイルや文字のドラッグ&ドロップができます。
また、tkinterのウィジェットのドラッグ&ドロップはtkinter.dndを使うとできるようです。(とても面倒くさそうですが...)

tkdndは2.8以降を推奨します。最新版は2.9.2でここからダウンロードできます。
2.8以下はここです。
ちなみになぜ2.8以降を推奨するかですが、2.8以降でないと、日本語ファイル名が文字化けしたり、Notebookにdndできないなど不具合があるからです。
以下に、TkinterDnD2の改良版及び、サンプルを置いておきます。

tkdnd.py
# -*- coding: utf-8 -*-

try:
    import Tkinter as tkinter
    import Tix as tix
    import ttk
except ImportError:
    import tkinter
    from tkinter import tix
    from tkinter import ttk
import os
TkdndVersion = None
# dnd actions
PRIVATE = 'private'
NONE = 'none'
ASK = 'ask'
COPY = 'copy'
MOVE = 'move'
LINK = 'link'
REFUSE_DROP = 'refuse_drop'
# dnd types
DND_TEXT = 'DND_Text'
DND_FILES = 'DND_Files'
DND_ALL = '*'
CF_UNICODETEXT = 'CF_UNICODETEXT'
CF_TEXT = 'CF_TEXT'
CF_HDROP = 'CF_HDROP'
FileGroupDescriptor = 'FileGroupDescriptor - FileContents'# ??
FileGroupDescriptorW = 'FileGroupDescriptorW - FileContents'# ??

def _require(tkroot):
    '''Internal function.'''
    global TkdndVersion
    tkdndlib = os.environ.get('TKDND_LIBRARY')
    if tkdndlib != "__null__":
        if tkdndlib:
            tkroot.tk.eval('global auto_path; lappend auto_path {%s}' % tkdndlib)
    try:
        TkdndVersion = tkroot.tk.call('package', 'require', 'tkdnd')
    except tkinter.TclError:
        raise RuntimeError('Unable to load tkdnd library.')
    return TkdndVersion

class DnDEvent:
    pass

class DnDWrapper:
    _subst_format_dnd = ('%A', '%a', '%b', '%C', '%c', '{%CST}',
                         '{%CTT}', '%D', '%e', '{%L}', '{%m}', '{%ST}',
                         '%T', '{%t}', '{%TT}', '%W', '%X', '%Y')
    _subst_format_str_dnd = " ".join(_subst_format_dnd)
    tkinter.BaseWidget._subst_format_dnd = _subst_format_dnd
    tkinter.BaseWidget._subst_format_str_dnd = _subst_format_str_dnd

    def _substitute_dnd(self, *args):
        if len(args) != len(self._subst_format_dnd):
            return args
        def getint_event(s):
            try:
                return int(s)
            except ValueError:
                return s
        def splitlist_event(s):
            try:
                return self.tk.splitlist(s)
            except ValueError:
                return s

        A, a, b, C, c, CST, CTT, D, e, L, m, ST, T, t, TT, W, X, Y = args
        ev = DnDEvent()
        ev.action = A
        ev.actions = splitlist_event(a)
        ev.button = getint_event(b)
        ev.code = C
        ev.codes = splitlist_event(c)
        ev.commonsourcetypes = splitlist_event(CST)
        ev.commontargettypes = splitlist_event(CTT)
        ev.data = D
        ev.name = e
        ev.types = splitlist_event(L)
        ev.modifiers = splitlist_event(m)
        ev.supportedsourcetypes = splitlist_event(ST)
        ev.sourcetypes = splitlist_event(t)
        ev.type = T
        ev.supportedtargettypes = splitlist_event(TT)
        try:
            ev.widget = self.nametowidget(W)
        except KeyError:
            ev.widget = W
        ev.x_root = getint_event(X)
        ev.y_root = getint_event(Y)
        return (ev,)
    tkinter.BaseWidget._substitute_dnd = _substitute_dnd

    def _dnd_bind(self, what, sequence, func, add, needcleanup=True):
        if isinstance(func, str):
            self.tk.call(what + (sequence, func))
        elif func:
            funcid = self._register(func, self._substitute_dnd, needcleanup)
            # FIXME: why doesn't the "return 'break'" mechanism work here??
            #cmd = ('%sif {"[%s %s]" == "break"} break\n' % (add and '+' or '',
            #                              funcid, self._subst_format_str_dnd))
            cmd = '%s%s %s' %(add and '+' or '', funcid,
                                    self._subst_format_str_dnd)
            self.tk.call(what + (sequence, cmd))
            return funcid
        elif sequence:
            return self.tk.call(what + (sequence,))
        else:
            return self.tk.splitlist(self.tk.call(what))
    tkinter.BaseWidget._dnd_bind = _dnd_bind

    def dnd_bind(self, sequence=None, func=None, add=None):
        return self._dnd_bind(('bind', self._w), sequence, func, add)
    tkinter.BaseWidget.dnd_bind = dnd_bind

    def drag_source_register(self, button=None, *dndtypes):
        if button is None:
            button = 1
        else:
            try:
                button = int(button)
            except ValueError:
                # no button defined, button is actually
                # something like DND_TEXT
                dndtypes = (button,) + dndtypes
                button = 1
        self.tk.call(
                'tkdnd::drag_source', 'register', self._w, dndtypes, button)
    tkinter.BaseWidget.drag_source_register = drag_source_register

    def drag_source_unregister(self):
        self.tk.call('tkdnd::drag_source', 'unregister', self._w)
    tkinter.BaseWidget.drag_source_unregister = drag_source_unregister

    def drop_target_register(self, *dndtypes):
        self.tk.call('tkdnd::drop_target', 'register', self._w, dndtypes)
    tkinter.BaseWidget.drop_target_register = drop_target_register

    def drop_target_unregister(self):
        self.tk.call('tkdnd::drop_target', 'unregister', self._w)
    tkinter.BaseWidget.drop_target_unregister = drop_target_unregister

    def platform_independent_types(self, *dndtypes):
        return self.tk.split(self.tk.call(
                            'tkdnd::platform_independent_types', dndtypes))
    tkinter.BaseWidget.platform_independent_types = platform_independent_types

    def platform_specific_types(self, *dndtypes):
        return self.tk.split(self.tk.call(
                            'tkdnd::platform_specific_types', dndtypes))
    tkinter.BaseWidget.platform_specific_types = platform_specific_types

    def get_dropfile_tempdir(self):
        return self.tk.call('tkdnd::GetDropFileTempDirectory')
    tkinter.BaseWidget.get_dropfile_tempdir = get_dropfile_tempdir

    def set_dropfile_tempdir(self, tempdir):
        self.tk.call('tkdnd::SetDropFileTempDirectory', tempdir)
    tkinter.BaseWidget.set_dropfile_tempdir = set_dropfile_tempdir

class Tk(tkinter.Tk, DnDWrapper):
    def __init__(self, *args, **kw):
        tkinter.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)

class TixTk(tix.Tk, DnDWrapper):
    def __init__(self, *args, **kw):
        tix.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)
sample.py
import os
os.environ['TKDND_LIBRARY'] = "[tkdndのpath]"
from tkinter import *
from tkinter.ttk import *
from tkdnd import *
import tkinter.dialog

root = Tk(className="Test")
root.title("tkdnd Test")

root.l1 = Label(root, text="Drop file here")
root.l1.pack(fill="both", expand=True, side="left")
root.l1.drop_target_register(DND_FILES)
root.l1.dnd_bind('<<Drop>>', lambda event: root.l1.configure(text=event.data))

root.l2 = Label(root, text="Drop text here")
root.l2.pack(fill="both", expand=True, side="right")
root.l2.drop_target_register(DND_TEXT)
root.l2.dnd_bind('<<Drop>>', lambda event: root.l2.configure(text=event.data))
root.mainloop()

クラスプラットフォーム化

クロスプラットフォーム化ですが、windowsとlinuxは簡単なんです。
(とはいってもだいぶ投げやりです。)
macはmacがないとできないようです。(できる方法をご存知でしたら、コメント下さい。)

Windows

pyinstallerでexe化します。
(なんだかエラーが多いようですが、それについてはいまのところ言及なし。)
ちなみに、linuxやMac上でこの方法を行うには、Wineを使うといいと思います。

#python2なら
pip install PyInstaller
#python3なら
pip3 install Pyinstaller
#windows上のpython3なら
py -3 -m pip inistall Pyinstaler
#以後、半永久的に続きそうなので、省略

pyinstaller [pythonスクリプトへのパス]
#エラーが出たら、
python3 -m PyInstaller [pythonスクリプトへのパス]

Linux

始めに、linux用ソフトウェアパッケージを1ディストリビューション分つくって各々のディストーション用パッケージにalienで変換します。
そのため、なんとかしてlinux環境を構築してください。
(windowsならWSLとか、macは不明)

macOS

環境が整ったので、後ほど追記します。

参考文献など

6
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
12