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 ロギング(Logger)

ソースコードのデバッグ情報表示は、何も準備せず使用可能なprint()を使用してる方が多いと思いますが、エンジニアとしては、logger(loggingモジュール)を使用することをお勧めします。理由は、ググれば他のサイトでも書かれていますが、まずは ログ表示(出力)レベルを任意に設定可能なこと。これは、linux kernelでも実装されていて、linuxドライバ開発で何度も使用しました。(ログレベルもlinuxと同一で、debug, info, warning, error, critical)
Pythonでは更に、.yaml(古い実装では.ini)ファイルを設けて、プログラムを変更することなくログ表示レベルやログ出力先や出力形式をカスタマイズできる点ですね。.yamlファイルを使用せず、ソース内に埋め込むことも可能。

実装は、クラス化するほどでもなかったので、当サイト内に掲載しているconfig_ini.pyモジュールに追加しました。
yamlを使用するのでinstallが必要です。インストールは、コンソールから”> pip install pyyaml”でインストール可能です。

以下のロギング機能ソースは、
①カレントフォルダ下にlogフォルダを作成して、フォルダ下のerror.logファイルの最終行にログを追加(error.logファイルはappendモード指定)します。
②モジュール(.py)ごとに、ログレベルとログ出力先(strerrとerror.logファイル)をカスタマイズしてます。

---config_ini.py---
# coding: utf-8
# yamlパッケージ pyyaml-6.0.3 (pip install pyyaml)
import yaml

LOG_DIR = "./log"
CONFIG_FILE = "config.yaml"

class ConfigIni():           # 既存config_iniクラス
    def __init__(self):      # 既存config_iniクラス__init__()メソッド
        try:
            # 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)
            ~以下省略~
---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

.yamlに記載した”loggers”の”debug1_tab”、”debug1_tab”モジュールと、debug1_tab.py、setting_tab.pyのgetLogger(__name__)文の__name__を一致させている。
モジュール内のロギング方法を以下に掲載します。warningより上のerror()、critical()は載せてません。

---debug1_tab.pyの一部---
import logging
class Debug1Tab(tk.Frame):
    def __init__(self, parent, config_ini):
        self.logger = logging.getLogger(__name__)
        self.logger.debug("Debログ")        # 確認コード。strerrのみ出力
        self.logger.info("Infoログ")        # 確認コード。strerr、ファイル出力
        self.logger.warning("Waringログ")   # 確認コード。strerr、ファイル出力
---setting_tab.pyの一部---
import logging
class SettingTab(tk.Frame):
    def __init__(self, parent, config_ini):
        self.logger = logging.getLogger(__name__)
        self.logger.debug("Debログ")        # 確認コード。出力なし
        self.logger.info("Infoログ")        # 確認コード。出力なし
        self.logger.warning("Waringログ")   # 確認コード。strerrのみ出力

stderrとログファイル出力結果。(太字にしましたが)stderr出力とファイル出力の違いが分かりますか?

---stderr出力---
2025-10-11 11:34:41,567 debug1_tab:15 __init__ DEBUG Debログ
2025-10-11 11:34:41,567 debug1_tab:16 __init__ INFO Infoログ
2025-10-11 11:34:41,567 debug1_tab:17 __init__ WARNING Waringログ
2025-10-11 11:34:41,581 setting_tab:13 __init__ WARNING Waringログ
---error.logファイル出力(sjis)---
2025-10-09 21:57:30,807 debug1_tab:16 __init__ INFO Infoログ
2025-10-09 21:57:30,807 debug1_tab:17 __init__ WARNING Waringログ
2025-10-11 11:34:41,567 debug1_tab:16 __init__ INFO Infoログ
2025-10-11 11:34:41,567 debug1_tab:17 __init__ WARNING Waringログ

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()      #設定ファイル更新

 

ProfileGridのセットアップ方法


このプラグインは日本語化すると、わかりやすくなります。設定がシンプルでセットアップが簡単でした。日本語言語ファイル< https://www.emb-se.com/download/ >はProfileGridのプログラム構成が変わってしまったので追従をあきらめ配信をやめました。


◆プラグイン有効化後、自分の思い描く会員制サイトにセットアップする手順(流れ)をここにメモしておきます。日本語化するため、このプラグイン下のlanguagesフォルダ下の.POと.MOのファイルを置き換えます。


1.[グループ管理]で以下節の設定を行う

1.1.[Default Group](ID=1)を[通常会員]に名称変更する。

1.2.この[通常会員]矩形内に[設定]と[フィールド]設定がある。マウスカーソルを持って行くと現れる。以下の節で[設定]と[フィールド]を設定する。


1.2.1.未登録会員が、この[通常会員]に加入する際に、登録画面(=[Register] 固定ページ)で未登録会員が入力する項目(=[プロフィール])を[フィールド]設定で、追加・削除する。項目の順序も自分で決められる。

1.2.2.未登録会員の承認方法や会員になった場所のWordPress 権限や未登録会員宛の会員仮登録メールを[設定]でセットアップする。


1.3.WordPress管理者が所属するグループを[管理Gr]として新規作成して、[管理Gr]矩形内の[設定]で権限を管理者にする。


2.[全般設定]のセットアップ[メールテンプレート]の編集。メール件名やメール本文を日本人向けにする。他のサブメニュー内の設定を好みでセットアップする。


3.ホームページにメニューを表示する。

3.1.[外観]で、ProfileGridが作成してくれた固定ページの[login], [Register], [My Profile], [for got password]をメニューにエントリーして、メニュー表示位置を決めてホームページにメニューを表示する。この時メニューの項目名も日本語に変更する。

 

[2022/7/14追記] 最近のバージョン4.9.xでは多機能になり、ProfilegGrid公式サイトにスターターガイド<https://profilegrid.co/profilegrid-starter-guide/>が公開されています。英文ですがCrome翻訳機能やGoogle先生に翻訳してもらえばよろしいかと思います。

linux, Windowsバックアップツール

OS(linux,Windows)バックアップツールをやっとみつけました。

Clonezilla Liveです。Clonezzila公式サイトからダウンロードできます。
機能を列挙しますと、
・ディスクイメージの保存・復元が可能
・パーティションの保存・復元が可能
・NASへの保存やNASからの復元が可能
・ディスク使用容量が少ないと速い。使用量14.2GBで全操作12分位でした。NAS保存の場合で。
linuxディスクイメージの保存と復元方法を記載しましたので、ご覧ください。

ProfileGridプラグインの日本語対応

このプラグインフォルダ下にlanguagesフォルダがあり、xxxx-jp.poとxxxx-jp.moが日本語化対応ファイルです。POEditor(https://poeditor.com/)で.poファイルを開いて鋭意、翻訳+意訳しました。結構な行数で大変でした。(日本語化すると、こんな感じになります⇒ProfileGridプラグイン日本語化

予めこのプラグインをいじってみないと、意味不明なGoogle翻訳を意訳 修正出来ませんでした。会員制サイトに利用できそうな部分は意訳できていると思います。私自身で別途会員サイトを構築してます。

ProfileGridが公式に日本語化されるのは、 現状を鑑みて 随分先の事になると思いますので、非公式にて公開< https://www.emb-se.com/download/ >しておりましたが、ProfileGridのプログラム構成が大分変わってしまったので追従することをあきらめました。

STM32CubeIDEでST-Link-V2が接続できない事象の対処方法

ここには簡潔に記載することにする。

【原因】

STM32CubeIDEのCode Generator(コードジェネレーター)で出力された以下、__HAL_AFIO_REMAP_SWJ_DISABLE(); のコードである。
__HAL_AFIO_REMAP_SWJ_DISABLE()は、JTAG-DPとSW-DPをDisabledにする設定なので ST-Link等のデバッガーが接続できなくなって当然である。

main()
 +- HAL_Init()
 |   +- HAL_MspInit()
 |       +- __HAL_AFIO_REMAP_SWJ_DISABLE() ★これが原因
 |

【対処】

HAL_MspInit()でコールしている__HAL_AFIO_REMAP_SWJ_DISABLE()を削除すれば良いのだが、ST-Linkが接続できないので、STM32 ST-LINK Utilityを使用してFLASHメモリに書き込んだプログラムを削除する必要がある。

【ST-LINK Utilityで接続する方法】

前提条件として、基板にRESETスイッチが必要である。(RESETスイッチが無い場合、基板に通電した状態でマイコン端子のNRSTをGNDに落とした状態がRESETスイッチON状態、GNDから離せばスイッチOFF状態ですが、場合によっては抵抗(10KΩ)を付けないと基板が死にます。RESETサブ基板を作りましょう)
手順①:ST-LINK Utilityを起動する。※ST社サイトからダウンロードできます。
手順②:基板のRESETスイッチを押し続ける。
手順③:ST-LINK Utilityの[Connect to the target]ボタンをクリックする。
手順④:基板のRESETスイッチを離す。
※接続できない場合、手順④の実施タイミングにより接続できない時があるので、手順②からやり直しましょう。
手順⑤:書き込んだセクターだけをメニューの[Erase sector]で消去する。
※[Full chip erase]ボタンでFLASHメモリの全領域を消去しても良いが、DFU(プログラムダウンローダー)まで消去される。私はDFUを使用せず、ST-Linkでプログラムを書き込んでいるので全領域消去してます。
※Erase sectorを調べるには、プログラムが書き込まれているアドレス範囲を調べる必要がある。debugまたはreleaseフォルダからxxxx.mapファイルを検索して、エディタで開いて確認しましょう。
手順⑥:[Disconnect from the target]ボタンをクリックする。

【対処完了】

これで、STM32CubeIDEでST-Link-V2が接続できます。原因を排除したプログラムでデバッグ再開できます。