Python GUI Tkinter

Python GUIであるTcl/Tkのウィジェットを用いてエンジニアらしいGUIアプリケーションの実装(クラス化)を展開しようと思います。因みに現在バージョンはPython 3.13、Tcl/Tk 8.6.13(確認方法>py -m tkinter)である。下に展開したソースは、Python GUIサンプルアプリとして第3弾目(PyQt6→PySide6→Tkinter向け)です。改善&機能追加した実装になってます。機能ごとにモジュール化(クラス化)した実装です。

使用した部品は<PySide6 GUIサンプルアプリ>に合わせて、メインウィンドウ(Frame)、タブコントロール(Notebook)、グループボックス(LabelFrame)、1行のエディトボックス(Entry)、複数行のエディトボックス(Text)、プッシュボタン(Button)、コンボボックス(Combobox)、ディレクトリ選択ダイアログ(QFileDialog.getExistingDirectory)、Windows風INIファイル(configparser.ConfigParser)、ロギング機能(logging, logger, handlers, formatters)です。

Tkinter GUIサンプルアプリ起動直後の画面と[設定]タブ画面

---main.py---
# coding: utf-8
# Tcl/Tk 8.6.13(確認方法>py -m tkinter)
#import sys
import tkinter as tk
import tkinter.ttk as ttk
# 独自モジュール
import config_ini
import debug1_tab, setting_tab


class MainWindow(tk.Frame):
    def __init__(self, main_win):
        main_win.title("Tkinter タブ クラス化")
        main_win.geometry("500x540")

        self.config_ini = config_ini.ConfigIni()
        self.config_ini.read_config_ini()

        # タブウィジェット生成
        self.tab_widget = ttk.Notebook(main_win)

        # [Debug1]タブGUI付加
        self.debug1_tab = debug1_tab.Debug1Tab(self.tab_widget, self.config_ini)
        self.tab_widget.add(self.debug1_tab, text="Debug1")
        # [設定]タブGUI付加
        self.setting_tab = setting_tab.SettingTab(self.tab_widget, self.config_ini)
        self.tab_widget.add(self.setting_tab, text="設定")

        # タブウィジェット配置
        self.tab_widget.pack(expand=True, fill='both', padx=4, pady=4)

        # ステータスバー表示は2通り
        frame = ttk.Frame(main_win, width=500)
        frame.pack(anchor=tk.W, side=tk.BOTTOM)
        self.status_bar = ttk.Label(frame, text="ここに情報を表示します", anchor=tk.W)
        self.status_bar.pack()
        # ステータスバーは、(上4行)Frame内にLabelを貼るか、(下2行)Frameを作らずLabelを貼るか。
        #self.status_bar = ttk.Label(main_win, text="ここに情報を表示します", anchor=tk.W)
        #self.status_bar.pack(anchor=tk.W, side=tk.BOTTOM)

if __name__ == "__main__":
    root = tk.Tk()
    main_win = MainWindow(root)
    root.mainloop()
    main_win.config_ini.uptate_config_ini()
---debug1_tab.py---
# coding: utf-8
# Tcl/Tk 8.6.13
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import filedialog
import logging

class Debug1Tab(tk.Frame):
    def __init__(self, parent, config_ini):
        super().__init__(parent)
        self.config_ini = config_ini
        self.logger = logging.getLogger(__name__)
        #self.logger.debug("Debログ")        # モジュール毎ロガー確認
        #self.logger.info("Infoログ")        # 
        #self.logger.warning("Waringログ")   # 

        label = ttk.Label(self, text="Debug-1 GUI")
        label.pack()

        groupbox = tk.LabelFrame(self, text="格納先フォルダ指定")
        groupbox.pack(padx=2, ipady=2)
        button = ttk.Button(groupbox)
        button.configure(text="フォルダ指定", command=self.folder_button_click)
        button.pack(anchor=tk.NW, side=tk.LEFT)
        self.folder_line_edit = ttk.Entry(groupbox, width=66)
        self.folder_line_edit.insert(tk.END, self.config_ini.save_folder)
        self.folder_line_edit.pack(anchor=tk.NW, side=tk.LEFT, pady=2)

        groupbox = tk.LabelFrame(self, text="アクション")
        #groupbox = tk.LabelFrame(self, text="アクション", width=400, height=94)
        #groupbox.propagate(False)
        groupbox.pack(anchor=tk.NW, side=tk.TOP, padx=2, ipady=2)
        frame = ttk.Frame(groupbox, width=80)
        frame.pack()
        button = ttk.Button(frame)
        button.configure(text="アクション1", command=self.action1_button_click)
        button.pack(side=tk.LEFT)
        self.combobox = ttk.Combobox(frame, values=["アイテム0", "アイテム1"])  #item=0,1
        self.combobox.current(0)
        self.combobox.pack(side=tk.LEFT)
        button = ttk.Button(groupbox)
        button.configure(text="アクション2", command=self.action2_button_click)
        button.pack(anchor=tk.W, side=tk.TOP)
        button = ttk.Button(groupbox)
        button.configure(text="アクション3", command=self.action3_button_click)
        button.pack(anchor=tk.W, side=tk.TOP)

        groupbox = tk.LabelFrame(self, text="実行情報")
        groupbox.pack(anchor=tk.NW, side=tk.TOP, padx=2, ipady=2)
        button = ttk.Button(groupbox)
        button.configure(text="表示クリア", command=self.clear_button_click)
        button.pack(anchor=tk.N)
        self.text_edit = tk.Text(groupbox, width=80, height=20)
        self.text_edit.pack(anchor=tk.W, padx=2, pady=2)

    def folder_button_click(self):
        folder = self.folder_line_edit.get()
        if folder == "":
            folder = "c:/"
        folder = filedialog.askdirectory(initialdir = folder)
        self.folder_line_edit.insert(tk.END, folder)
        self.config_ini.save_folder = folder

    def action1_button_click(self):
        self.item_index = self.combobox.current()
        self.text_edit.insert(tk.END, "action1_button clicked  item={}\n".format(self.item_index))

    def action2_button_click(self):
        self.text_edit.insert(tk.END, "action2_button clicked\n")

    def action3_button_click(self):
        self.text_edit.insert(tk.END, "action3_button clicked\n")

    def clear_button_click(self):
        self.text_edit.delete("1.0", "end")
---setting_tab.py---
# coding: utf-8
# Tcl/Tk 8.6.13
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import filedialog
import logging

class SettingTab(tk.Frame):
    def __init__(self, parent, config_ini):
        super().__init__(parent)
        self.config_ini = config_ini
        self.logger = logging.getLogger(__name__)
        #self.logger.debug("Debログ")        # ロギング確認
        #self.logger.info("Infoログ")        #
        #self.logger.warning("Waringログ")   #

        label = ttk.Label(self, text="Settings GUI")
        label.pack()

        groupbox = tk.LabelFrame(self, text="格納先フォルダ指定")
        groupbox.pack(padx=2, ipady=2)
        folder_button = ttk.Button(groupbox)
        folder_button.configure(text="フォルダ指定", command=self.folder_button_click)
        folder_button.pack(anchor=tk.NW, side=tk.LEFT)
        self.folder_line_edit = ttk.Entry(groupbox, width=100)
        self.folder_line_edit.insert(tk.END, self.config_ini.save_folder)
        self.folder_line_edit.pack(anchor=tk.NW, side=tk.LEFT, pady=2)

        groupbox = tk.LabelFrame(self, text="プログラム情報")
        groupbox.pack(padx=2, ipady=2)
        self.text_edit = tk.Text(groupbox, width=80, height=20)
        self.text_edit.pack(padx=2, pady=2)
        self.text_edit.insert(tk.END, "Version : {}\n".format(self.config_ini.latest_ver))
        self.text_edit.insert(tk.END, "Date    : {}\n".format(self.config_ini.latest_date))
        self.text_edit.insert(tk.END, "{}\n".format(self.config_ini.latest_overview))
        self.text_edit.config(state="disabled")     #リードオンリー

    def folder_button_click(self):
        folder = self.folder_line_edit.get()
        if folder == "":
            folder = "c:/"
        folder = filedialog.askdirectory(initialdir = folder)
        self.folder_line_edit.insert(tk.END, folder)
        self.config_ini.save_folder = folder
---config_ini.py---
# coding: utf-8
# yamlパッケージ pyyaml-6.0.3 (pip install pyyaml)

# config.ini関連
import configparser
import os
# logging関連
import logging
import logging.config
import yaml

# config.ini関連
CONFIG_INI_FILE = "config.ini"
DEFAULT_SECTION = "DEFAULT"
SETTING_SECTION = "SETTING"
SAVE_FOLDER = "save_folder"
LATEST_SECTION = "LATEST"
RELEASE_VER = "version"
RELEASE_DATE = "date"
RELEASE_OVERVIEW = "overview"

# logging関連
LOG_DIR = "./log"
CONFIG_FILE = "config.yaml"


class ConfigIni():
    def __init__(self):
        # .iniファイルが存在しない場合、キーの設定値に文字列をセットしてプログラム実行を続行する
        # 尚、キーの設定値にNoneをセットして(プログラム)処理にてExcetsion発生させ中断することも可能
        self.default_folder = ""
        self.save_folder = ""
        self.latest_ver = ""
        self.latest_date = ""
        self.latest_overview = ""
        try:
            self.config_ini = configparser.ConfigParser()
            if not os.path.exists(CONFIG_INI_FILE):
                raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), CONFIG_INI_FILE)

            # config.yamlの内容をloggingに登録する
            if not os.path.exists(LOG_DIR):
                os.mkdir(LOG_DIR)
            with open(CONFIG_FILE, "r") as fp:
                config = yaml.safe_load(fp)
            logging.config.dictConfig(config)

        except Exception as err:
            print(err)

    def read_config_ini(self):
        retval = False
        try:
            #self.config_ini.read(CONFIG_INI_FILE, encoding='utf-8')
            # Python 3.2以降のencoding指定(推奨)ぽい…AIによると
            with open(CONFIG_INI_FILE, 'r', encoding='utf-8') as fp:
                self.config_ini.read_file(fp)
            section = self.config_ini[DEFAULT_SECTION]
            self.default_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[SETTING_SECTION]
            self.save_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[LATEST_SECTION]
            self.latest_ver = section.get(RELEASE_VER)
            self.latest_date = section.get(RELEASE_DATE)
            self.latest_overview = section.get(RELEASE_OVERVIEW)
            retval = True
        except Exception as err:
            print(err)
        return retval

    def uptate_config_ini(self):
        retval = False
        try:
            self.config_ini.set(DEFAULT_SECTION, SAVE_FOLDER, self.default_folder)
            self.config_ini.set(SETTING_SECTION, SAVE_FOLDER, self.save_folder)
            # Python 3.2以降のencoding指定(推奨)方法
            with open(CONFIG_INI_FILE, 'w', encoding='utf-8') as fp:
                self.config_ini.write(fp)
            retval = True
        except Exception as err:
            print(err)
        return retval
---config.ini---
[DEFAULT]
save_folder = C:/

[SETTING]
save_folder = C:/Python

[LATEST]
version = V1.0.0
date = 25/10/20
overview = ここにリリース概要を記載する
---config.yaml---
version: 1
formatters:
  defaultFormat:
    format: '%(asctime)s %(name)s:%(lineno)s %(funcName)s %(levelname)s %(message)s'
handlers:
  consoleHandler:
    class: logging.StreamHandler
    formatter: defaultFormat
    level: DEBUG
  fileHandler:
    class: logging.FileHandler
    formatter: defaultFormat
    level: INFO
    filename: ./log/error.log
    mode: a
loggers:
  debug1_tab:
    handlers:
      - consoleHandler
      - fileHandler
    level: DEBUG
    propagate: no
  setting_tab:
    handlers:
      - consoleHandler
    level: WARNING
    propagate: no
root:
  handlers:
    - consoleHandler
  level: INFO

ロガーの出力結果は、当サイト「Python ロギング(Logger)」をご確認ください。

Python GUI PySide6

Python GUIであるPySide6のウィジェットを用いてエンジニアらしい実装を展開しようと思います。因みに現在バージョンはPython 3.13、PyQt6 6.9.3である。当サイト「Python GUI PyQt6」のGUIサンプルアプリソースを機能ごとにモジュール化(クラス化)した実装ソースを下部に展開する。

作成するメソッドは、(全て対応できてないが)プログラムが落ちないようにtry〜exceptを使いましょう。メインスレッド(main.py等)にコテコテ実装せず、機能ごとに別ソースファイル(.py)にclassを作りメイン等にimportして実装するようにしましょう。このようにすると、メインソースファイルや機能ソースファイルの流用が簡単ですし、機能の切った貼ったも簡単ですよね。

使用した PySide6 部品は<PyQt6サンプルアプリ>同様、メインウィンドウ(QMainWindow)、ステータスバー(QStatusBar)、タブコントロール(QTabWidget)、グループボックス(QGroupBox)、1行のエディトボックス(QLineEdit)、複数行のエディトボックス(QTextEdit)、プッシュボタン(QPushButton)、コンボボックス(QComboBox)、ディレクトリ選択ダイアログ(QFileDialog.getExistingDirectory)、ボタンウィジェットにスタイル設定(setStyleSheet)、Windows風INIファイルです(configparser.ConfigParser)。

PySide6 サンプルアプリ起動直後の画面と[設定]タブ画面

---main.py---
# coding: utf-8
# PySide6 6.9.3
import sys
from PySide6.QtWidgets import QApplication,QMainWindow,QTabWidget,QWidget,QLabel,QVBoxLayout, QGroupBox,QHBoxLayout,QPushButton,QTextEdit,QLineEdit,QFileDialog,QComboBox,QSpacerItem
# 独自モジュール
import config_ini
import debug1_tab, setting_tab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 タブ クラス化")
        self.resize(500, 540)

        self.config_ini = config_ini.ConfigIni()
        self.config_ini.read_config_ini()
        # QTabWidgetを作成
        self.tab_widget = QTabWidget()

        # 各タブのコンテンツクラスを作成し、QTabWidgetに追加
        self.tab_widget.addTab(debug1_tab.Debug1Tab(self.config_ini), "Debug1")
        self.tab_widget.addTab(setting_tab.SettingsTab(self.config_ini), "設定")

        self.setCentralWidget(self.tab_widget)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    retval = app.exec()
    window.config_ini.uptate_config_ini()
    sys.exit(retval)
---debug1_tab.py---
# coding: utf-8
# PySide6 6.9.3
import sys
from PySide6.QtWidgets import QApplication,QMainWindow,QTabWidget,QWidget,QLabel,QVBoxLayout, QGroupBox,QHBoxLayout,QPushButton,QTextEdit,QLineEdit,QFileDialog,QComboBox,QSpacerItem

class Debug1Tab(QWidget):
    def __init__(self, config_ini):
        self.GUI_STYLE_QSS = """
                            QPushButton {
                                background-color: #f4f4f4;
                            }
                            QPushButton:pressed {
                                background-color: white;
                            }
                            QStatusBar {
                                border-width: 1px;
                                border-style: solid;
                                border-color: #efefef;
                            }
                        """
        self.config_ini = config_ini

        super().__init__()
        tab_layout = QVBoxLayout()
        tab_layout.addWidget(QLabel("Debug 1 GUI"))     # [QLabel見本]デザイン的には無しまたは中央表示
        self.setLayout(tab_layout)

        #self.save_folder = ""
        group_box = QGroupBox("格納先フォルダ指定")
        tab_layout.addWidget(group_box)
        group_layout = QHBoxLayout()
        group_box.setLayout(group_layout)
        self.folder_button = QPushButton("フォルダ指定")
        self.folder_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.folder_button.setFixedSize(100, 24)
        self.folder_button.clicked.connect(self.folder_button_click)
        group_layout.addWidget(self.folder_button)
        self.line_edit = QLineEdit()
        self.line_edit.setText(self.config_ini.save_folder)
        #self.line_edit.setText(self.save_folder)
        group_layout.addWidget(self.line_edit)

        group_box = QGroupBox("アクション")
        tab_layout.addWidget(group_box)
        group_layout = QVBoxLayout()
        group_box.setLayout(group_layout)

        action1_inner_layout = QHBoxLayout()
        group_layout.addLayout(action1_inner_layout)
        self.action1_button = QPushButton("アクション1")
        self.action1_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.action1_button.setFixedSize(100, 24)
        self.action1_button.clicked.connect(self.action1_button_click)
        action1_inner_layout.addWidget(self.action1_button)
        self.combobox = QComboBox()
        self.combobox.setFixedSize(100, 24)
        self.combobox.addItem("アイテム0")      #item=0
        self.combobox.addItem("アイテム1")
        action1_inner_layout.addWidget(self.combobox)
        action1_inner_layout.addSpacing(250)

        self.action2_button = QPushButton("アクション2")
        self.action2_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.action2_button.setFixedSize(100, 24)
        self.action2_button.clicked.connect(self.action2_button_click)
        group_layout.addWidget(self.action2_button)
        self.action3_button = QPushButton("アクション3")
        self.action3_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.action3_button.setFixedSize(100, 24)
        self.action3_button.clicked.connect(self.action3_button_click)
        group_layout.addWidget(self.action3_button)

        group_box = QGroupBox("実行情報")
        tab_layout.addWidget(group_box)
        group_layout = QVBoxLayout()
        group_box.setLayout(group_layout)
        self.clear_button = QPushButton("表示クリア")
        self.clear_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.clear_button.setFixedSize(100, 24)
        self.clear_button.clicked.connect(self.clear_button_click)
        group_layout.addWidget(self.clear_button)
        self.text_edit = QTextEdit()
        group_layout.addWidget(self.text_edit)

    def folder_button_click(self):
        folder = self.line_edit.text()
        #folder = self.save_folder
        if folder == "":
            folder = "c:/"
        folder = QFileDialog.getExistingDirectory(self, "格納先フォルダ選択", folder)
        self.line_edit.setText(folder)
        self.config_ini.save_folder = folder

    def action1_button_click(self):
        self.item_index = self.combobox.currentIndex()
        self.text_edit.append("action1_button clicked  item={}".format(self.item_index))

    def action2_button_click(self):
        self.text_edit.append("action2_button clicked")

    def action3_button_click(self):
        self.text_edit.append("action3_button clicked")

    def clear_button_click(self):
        self.text_edit.clear()
---setting_tab.py---
# coding: utf-8
# PySide6 6.9.3
import sys
from PySide6.QtWidgets import QApplication,QMainWindow,QTabWidget,QWidget,QLabel,QVBoxLayout, QGroupBox,QHBoxLayout,QPushButton,QTextEdit,QLineEdit,QFileDialog,QComboBox,QSpacerItem

# 各タブのコンテンツクラス化
class SettingsTab(QWidget):
    def __init__(self, config_ini):
        self.GUI_STYLE_QSS = """
                            QPushButton {
                                background-color: #f4f4f4;
                            }
                            QPushButton:pressed {
                                background-color: white;
                            }
                            QStatusBar {
                                border-width: 1px;
                                border-style: solid;
                                border-color: #efefef;
                            }
                        """
        self.config_ini = config_ini

        super().__init__()
        tab_layout = QVBoxLayout()
        tab_layout.addWidget(QLabel("Settings GUI"))
        self.setLayout(tab_layout)

        group_box = QGroupBox("デフォルト - 格納先フォルダ")
        tab_layout.addWidget(group_box)
        group_layout = QHBoxLayout()
        group_box.setLayout(group_layout)
        self.folder_button = QPushButton("フォルダ指定")
        self.folder_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.folder_button.setFixedSize(100, 24)
        self.folder_button.clicked.connect(self.def_folder_button_click)
        group_layout.addWidget(self.folder_button)
        self.line1_edit = QLineEdit()
        self.line1_edit.setText(self.config_ini.default_folder)
        group_layout.addWidget(self.line1_edit)

        group_box = QGroupBox("格納先フォルダ指定")
        tab_layout.addWidget(group_box)
        group_layout = QHBoxLayout()
        group_box.setLayout(group_layout)
        self.folder_button = QPushButton("フォルダ指定")
        self.folder_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.folder_button.setFixedSize(100, 24)
        self.folder_button.clicked.connect(self.save_folder_button_click)
        group_layout.addWidget(self.folder_button)
        self.line2_edit = QLineEdit()
        self.line2_edit.setText(self.config_ini.save_folder)
        group_layout.addWidget(self.line2_edit)
        group_box = QGroupBox("プログラム情報")
        tab_layout.addWidget(group_box)
        group_layout = QHBoxLayout()
        group_box.setLayout(group_layout)

        self.text_edit = QTextEdit()
        self.text_edit.append("Version : {}".format(self.config_ini.latest_ver))
        self.text_edit.append("Date    : {}".format(self.config_ini.latest_date))
        self.text_edit.append("{}".format(self.config_ini.latest_overview))
        self.text_edit.setReadOnly(True)
        group_layout.addWidget(self.text_edit)

    def def_folder_button_click(self):
        folder = self.line1_edit.text()
        if folder == "":
            folder = "c:/"
        folder = QFileDialog.getExistingDirectory(self, "格納先フォルダ選択", folder)
        self.line1_edit.setText(folder)
        self.config_ini.default_folder = folder

    def save_folder_button_click(self):
        folder = self.line2_edit.text()
        if folder == "":
            folder = "c:/"
        folder = QFileDialog.getExistingDirectory(self, "格納先フォルダ選択", folder)
        self.line2_edit.setText(folder)
        self.config_ini.save_folder = folder
---config_ini.py---
# coding: utf-8
import configparser
import os

CONFIG_INI_FILE = "config.ini"
DEFAULT_SECTION = "DEFAULT"
SETTING_SECTION = "SETTING"
SAVE_FOLDER = "save_folder"
LATEST_SECTION = "LATEST"
RELEASE_VER = "version"
RELEASE_DATE = "date"
RELEASE_OVERVIEW = "overview"

class ConfigIni():
    def __init__(self):
        # .iniファイルが存在しない場合、キーの設定値に文字列をセットしてプログラム実行を続行する
        # 尚、キーの設定値にNoneをセットして(プログラム)処理にてExcetsion発生させ中断することも可能
        self.default_folder = ""
        self.save_folder = ""
        self.latest_ver = ""
        self.latest_date = ""
        self.overview = ""
        try:
            self.config_ini = configparser.ConfigParser()
            if not os.path.exists(CONFIG_INI_FILE):
                raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), CONFIG_INI_FILE)
        except Exception as err:
            print(err)

    def read_config_ini(self):
        retval = False
        try:
            #self.config_ini.read(CONFIG_INI_FILE, encoding='utf-8')
            # Python 3.2以降のencoding指定(推奨)ぽい…AIによると
            with open(CONFIG_INI_FILE, 'r', encoding='utf-8') as fp:
                self.config_ini.read_file(fp)
            section = self.config_ini[DEFAULT_SECTION]
            self.default_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[SETTING_SECTION]
            self.save_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[LATEST_SECTION]
            self.latest_ver = section.get(RELEASE_VER)
            self.latest_date = section.get(RELEASE_DATE)
            self.overview = section.get(RELEASE_OVERVIEW)
            retval = True
        except Exception as err:
            print(err)
        return retval

    def uptate_config_ini(self):
        retval = False
        try:
            self.config_ini.set(DEFAULT_SECTION, SAVE_FOLDER, self.default_folder)
            self.config_ini.set(SETTING_SECTION, SAVE_FOLDER, self.save_folder)
            # Python 3.2以降のencoding指定(推奨)方法
            with open(CONFIG_INI_FILE, 'w', encoding='utf-8') as fp:
                self.config_ini.write(fp)
            retval = True
        except Exception as err:
            print(err)
        return retval
---config.ini---
[DEFAULT]
save_folder = C:/

[SETTING]
save_folder = C:/Python

[LATEST]
version = V1.0.0
date = 25/10/20
overview = ここにリリース概要を記載する

Python GUI PyQt6

Python GUIであるPyQt6のウィジェットを用いてエンジニアらしい実装を展開しようと思います。因みに現在バージョンはPython 3.13、PyQt6 6.9.1である。

作成するメソッドは、プログラムが落ちないようにtry〜exceptを使いましょう。メインスレッド(main.py等)にコテコテ実装せず、機能ごとに別ソースファイル(.py)にclassを作りメイン等にimportして実装するようにしましょう。このようにすると、メインソースファイルや機能ソースファイルの流用が簡単ですし、機能の切った貼ったも簡単ですよね。しかしならが、下のサンプルソースはコテコテ実装になってます(オブジェクト指向言語初心者や、Python GUIウィジェットをググりながら作ると、こんなソースコードになると思います)。
 クラス化した実装は、当サイト「Python GUI PySide6」(PySide6特有パラメータなど未使用なので import行をPyQt6に変更すれば動作すると思います)に実装しました。また PySide6側ソースと比較すれば、クラス化改修案が見えてくると思います。

使用した PyQt6 部品は、メインウィンドウ(QMainWindow)、ステータスバー(QStatusBar)、タブコントロール(QTabWidget)、グループボックス(QGroupBox)、1行のエディトボックス(QLineEdit)、複数行のエディトボックス(QTextEdit)、プッシュボタン(QPushButton)、コンボボックス(QComboBox)、ディレクトリ選択ダイアログ(QFileDialog.getExistingDirectory)、[表示クリア]ボタンウィジェットにスタイル設定(setStyleSheet)、Windows風INIファイルです(configparser.ConfigParser)。

※補足事項、[Debug2]タブ、[設定(Setting)]タブには部品は実装してません。[設定]タブについて、当サイト「Python GUI PySide6」や「Python GUI Tkinter」に実装してます。

PyQt6サンプルアプリ起動直後の画面

---main.py---
# coding: utf-8
import sys
import os
from PyQt6.QtWidgets import QApplication,QMainWindow,QStatusBar,QWidget,QTabWidget,QFrame,QHBoxLayout,QVBoxLayout,QLabel,QPushButton,QGroupBox,QTextEdit,QLineEdit,QFileDialog,QComboBox,QSpacerItem
# 独自モジュール(パッケージ)
import config_ini

#class MainWindow(QWidget):         #statusBarを付けるためQMainWindowに変更
class MainWindow(QMainWindow):
    def __init__(self, config_ini):
        super().__init__()
        self.setWindowTitle('PyQt6 サンプル')
        self.resize(500, 540)
        tab_widget = QTabWidget(self)
        tab_widget.setFixedSize(500,520)

        self.GUI_STYLE_QSS = """
                            QPushButton {
                                background-color: #efefef;
                            }
                            QPushButton:pressed {
                                background-color: white;
                            }
                            QStatusBar {
                                border-width: 1px;
                                border-style: solid;
                                border-color: lightgray;
                            }
                        """
        self.config_ini = config_ini

        self.init_debug1_tab(tab_widget)
        self.init_debug2_tab(tab_widget)
        self.init_setting_tab(tab_widget)

        status_bar = QStatusBar()
        self.setStatusBar(status_bar)
        status_bar.setStyleSheet(self.GUI_STYLE_QSS)
        status_bar.showMessage("ここに情報を表示します")

    def init_debug1_tab(self, tab_widget):
        tab_win = QWidget(self)
        tab_widget.addTab(tab_win, "Debug1")

        tab_layout = QVBoxLayout()
        tab_win.setLayout(tab_layout)

        group_box = QGroupBox("格納先フォルダ指定")
        tab_layout.addWidget(group_box)
        group_layout = QHBoxLayout()
        group_box.setLayout(group_layout)
        self.folder_button = QPushButton("フォルダ指定")
        #print(self.folder_button.sizeHint())            # 自動調整サイズ表示
        self.folder_button.setFixedSize(100, 24)
        self.folder_button.clicked.connect(self.folder_button_click)
        group_layout.addWidget(self.folder_button)
        self.line_edit = QLineEdit()
        self.line_edit.setText(self.config_ini.save_folder)
        group_layout.addWidget(self.line_edit)

        group_box = QGroupBox("アクション")
        tab_layout.addWidget(group_box)
        group_layout = QVBoxLayout()
        group_box.setLayout(group_layout)

        action1_inner_layout = QHBoxLayout()
        group_layout.addLayout(action1_inner_layout)
        self.action1_button = QPushButton("アクション1")
        self.action1_button.setFixedSize(100, 24)
        self.action1_button.clicked.connect(self.action1_button_click)
        action1_inner_layout.addWidget(self.action1_button)
        self.combobox = QComboBox()
        self.combobox.setFixedSize(100, 24)
        self.combobox.addItem("アイテム1")      #item=0
        self.combobox.addItem("アイテム2")
        action1_inner_layout.addWidget(self.combobox)
        action1_inner_layout.addSpacing(250)

        self.action2_button = QPushButton("アクション2")
        self.action2_button.setFixedSize(100, 24)
        self.action2_button.clicked.connect(self.action2_button_click)
        group_layout.addWidget(self.action2_button)
        self.action3_button = QPushButton("アクション3")
        self.action3_button.setFixedSize(100, 24)
        self.action3_button.clicked.connect(self.action3_button_click)
        group_layout.addWidget(self.action3_button)

        group_box = QGroupBox("実行情報")
        tab_layout.addWidget(group_box)
        group_layout = QVBoxLayout()
        group_box.setLayout(group_layout)
        self.clear_button = QPushButton("表示クリア")
        self.clear_button.setStyleSheet(self.GUI_STYLE_QSS)
        self.clear_button.setFixedSize(100, 24)
        self.clear_button.clicked.connect(self.clear_button_click)
        group_layout.addWidget(self.clear_button)
        self.text_edit = QTextEdit()
        group_layout.addWidget(self.text_edit)

    def action1_button_click(self):
        self.item_index = self.combobox.currentIndex()
        self.text_edit.append("action1_button clicked  item={}".format(self.item_index))

    def action2_button_click(self):
        self.text_edit.append("action2_button clicked")

    def action3_button_click(self):
        self.text_edit.append("action3_button clicked")

    def folder_button_click(self):
        folder = self.config_ini.save_folder
        if folder == "":
            folder = "c:/"
        folder = QFileDialog.getExistingDirectory(self, "格納先フォルダ選択", folder)
        self.line_edit.setText(folder)
        self.config_ini.save_folder = folder

    def clear_button_click(self):
        self.text_edit.clear()

    def init_debug2_tab(self, tab_widget):
        tab_win = QWidget(self)
        tab_widget.addTab(tab_win, "Debug2")

        tab_layout = QVBoxLayout()
        tab_win.setLayout(tab_layout)
        group_box = QGroupBox("Debug 2")
        tab_layout.addWidget(group_box)

    def init_setting_tab(self, tab_widget):
        tab_win = QWidget(self)
        tab_widget.addTab(tab_win, "設定")

        tab_layout = QVBoxLayout()
        tab_win.setLayout(tab_layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    # プログラムオープニング処理
    config_ini = config_ini.ConfigIni()
    config_ini.read_config_ini()
    # GUI(表示&操作)処理
    window = MainWindow(config_ini)
    window.show()
    retval = app.exec()
    # プログラム終了処理
    config_ini.uptate_config_ini()
    sys.exit(retval)
---config_ini.py---
# coding: utf-8
import configparser
import os

CONFIG_INI_FILE = "config.ini"
SETTING_SECTION = "SETTING"
SAVE_FOLDER = "save_folder"
LATEST_SECTION = "LATEST"
RELEASE_VER = "version"
RELEASE_DATE = "date"
RELEASE_OVERVIEW = "overview"

class ConfigIni():
    def __init__(self):
        # .iniファイルが存在しない場合、キーの設定値に文字列をセットしてプログラム実行を続行する
        # 尚、キーの設定値にNoneをセットして(プログラム)処理にてExcetsion発生させ中断することも可能
        self.save_folder = ""
        self.latest_ver = ""
        self.latest_date = ""
        self.latest_details = ""
        try:
            self.config_ini = configparser.ConfigParser()
            if not os.path.exists(CONFIG_INI_FILE):
                raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), CONFIG_INI_FILE)
        except Exception as err:
            print(err)

    def read_config_ini(self):
        retval = False
        try:
            #self.config_ini.read(CONFIG_INI_FILE, encoding='utf-8')
            # Python 3.2以降のencoding指定(推奨)ぽい…AIによると
            with open(CONFIG_INI_FILE, 'r', encoding='utf-8') as fp:
                self.config_ini.read_file(fp)
            setting_section = self.config_ini[SETTING_SECTION]
            self.save_folder = setting_section.get(SAVE_FOLDER)
            latest_section = self.config_ini[LATEST_SECTION]
            self.latest_ver = latest_section.get(RELEASE_VER)
            self.latest_date = latest_section.get(RELEASE_DATE)
            self.latest_details = latest_section.get(RELEASE_OVERVIEW)
            retval = True
        except Exception as err:
            print(err)
        return retval

    def uptate_config_ini(self):
        retval = False
        try:
            self.config_ini.set(SETTING_SECTION, SAVE_FOLDER, self.save_folder)
            # Python 3.2以降のencoding指定(推奨)方法
            with open(CONFIG_INI_FILE, 'w', encoding='utf-8') as fp:
                self.config_ini.write(fp)
            retval = True
        except Exception as err:
            print(err)
        return retval
---config.ini---
[DEFAULT]
save_folder = C:/

[SETTING]
save_folder = C:/Python

[LATEST]
version = V1.00
date = 25/10/20
overview = ここにリリース概要を記載する

近日実装予定内容は、
●まず、タブコントロール部分を別ソースファイルに移行(機能を部品化) ・・・PySide6サンプル公開中
●その後、QThreadを使用して、ボタン押下後の時間が掛かる処理をサブスレッド化(これも別ソースファイル化)
●そして、Tcl/Tk GUIでは困難な機能のサブスレッドにてプログレス表示
以上です。

Python INIファイル

Python GUIアプリでは、よく設定ファイルが必要になると思います。Windowsアプリでは、.iniファイルを設定ファイルにすることが多いと思います。
そこで、Python GUIアプリでもアプリ設定を.iniファイルで実現しようと思います。

このアプリ設定(.iniファイル)は、読み込みアクション(必須)と更新アクション(任意)を用意して再利用可能なようにクラス化して実装しました。このクラスは、アプリで使用するGUIパッケージ(モジュール)との関連を持たないので、設定項目のGUIが必要なら別途実装する必要があります。
当サイト内では、PySide6PyQt6TkinterのGUIサンプルアプリにて利用してます。

---config_ini.py---
# coding: utf-8
import configparser
import os

CONFIG_INI_FILE = "config.ini"
DEFAULT_SECTION = "DEFAULT"
SETTING_SECTION = "SETTING"
SAVE_FOLDER = "save_folder"
LATEST_SECTION = "LATEST"
RELEASE_VER = "version"
RELEASE_DATE = "date"
RELEASE_OVERVIEW = "overview"

class ConfigIni():
    def __init__(self):
        # .iniファイルが存在しない場合、キーの設定値に文字列をセットしてプログラム実行を続行する
        # 尚、キーの設定値にNoneをセットして(プログラム)処理にてExcetsion発生させ中断することも可能
        self.default_folder = ""
        self.save_folder = ""
        self.latest_ver = ""
        self.latest_date = ""
        self.latest_overview = ""
        try:
            self.config_ini = configparser.ConfigParser()
            if not os.path.exists(CONFIG_INI_FILE):
                raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), CONFIG_INI_FILE)
        except Exception as err:
            print(err)

    def read_config_ini(self):
        retval = False
        try:
            #self.config_ini.read(CONFIG_INI_FILE, encoding='utf-8')
            # Python 3.2以降のencoding指定(推奨)ぽい…AIによると
            with open(CONFIG_INI_FILE, 'r', encoding='utf-8') as fp:
                self.config_ini.read_file(fp)
            section = self.config_ini[DEFAULT_SECTION]
            self.default_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[SETTING_SECTION]
            self.save_folder = section.get(SAVE_FOLDER)
            section = self.config_ini[LATEST_SECTION]
            self.latest_ver = section.get(RELEASE_VER)
            self.latest_date = section.get(RELEASE_DATE)
            self.latest_overview = section.get(RELEASE_OVERVIEW)
            retval = True
        except Exception as err:
            print(err)
        return retval

    def uptate_config_ini(self):
        retval = False
        try:
            self.config_ini.set(DEFAULT_SECTION, SAVE_FOLDER, self.default_folder)
            self.config_ini.set(SETTING_SECTION, SAVE_FOLDER, self.save_folder)
            # Python 3.2以降のencoding指定(推奨)方法
            with open(CONFIG_INI_FILE, 'w', encoding='utf-8') as fp:
                self.config_ini.write(fp)
            retval = True
        except Exception as err:
            print(err)
        return retval
---config.ini---
[DEFAULT]
save_folder = C:/

[SETTING]
save_folder = C:/Python

[LATEST]
version = V1.0.0
date = 25/10/20
overview = ここにリリース概要を記載する

設定ファイルの読み込みは GUIアプリのGUI表示前(前段)に行い、更新(書き込み)はGUIメインループ後(最終段)に行います。

PySide6やPyQt6のmain.pyでは こんな感じ。またはGUI表示開始の__init__()内先頭にて実装します。サイト内GUIサンプルアプリは後者の実装です。

---main.py---
import config_ini

if __name__ == "__main__":
    app = QApplication(sys.argv)
    config_ini = config_ini.ConfigIni()
    config_ini.read_config_ini()        #設定ファイル読み込み
    #GUI表示(割愛):GUI表示クラス生成時の引数(パラメータ)にconfig_iniを渡してもよい
    retval = app.exec()    #GUIメインループ
    config_ini.uptate_config_ini()      #設定ファイル更新
    sys.exit(retval)

Tkinterのmain.pyではこんな感じ。またはGUI表示開始の__init__()内先頭にて実装します。サイト内GUIサンプルは後者の実装です。

---main.py---
import config_ini

if __name__ == "__main__":
    root = tk.Tk()
    config_ini = config_ini.ConfigIni()
    config_ini.read_config_ini()        #設定ファイル読み込み
    #GUI表示(割愛):GUI表示クラス生成時の引数(パラメータ)にconfig_iniを渡してもよい
    root.mainloop()    #GUIメインループ
    config_ini.uptate_config_ini()      #設定ファイル更新