Author: Nako

How to detect QTreeWidgetItem is double-clicked in PySide

GOAL

Today’s goal is to execute a function when QTreeWidgetItem is double clicked.

Environment

Windows 10
Python 3.8.7
PySide2 (5.15.2)

Method

First I created a sample QTreeWidget as below.

import sys
from PySide2.QtWidgets import *


class MyMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        self._generate_ui()
        self._init_tree_widget()

    def _generate_ui(self):
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
        self.tree_widget = QTreeWidget()
        self.tree_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.tree_widget.setSelectionBehavior(QAbstractItemView.SelectItems)
        main_layout.addWidget(self.tree_widget)

    def _init_tree_widget(self):
        headers = ["header1", "header2"]
        self.tree_widget.setHeaderLabels(headers)
        tree_widget_item1 = QTreeWidgetItem(["group1"])
        tree_widget_item1.addChild(QTreeWidgetItem(["item1_1", "item1_2"]))
        self.tree_widget.addTopLevelItem(tree_widget_item1)
        tree_widget_item2 = QTreeWidgetItem(["group2"])
        self.tree_widget.addTopLevelItem(tree_widget_item2)
        tree_widget_item2.addChild(QTreeWidgetItem(["item2_1", "item2_2"]))
        tree_widget_item2.addChild(QTreeWidgetItem(["item3_1", "item3_2"]))


def launch():
    app = QApplication.instance()
    if not app:
        app = QApplication(sys.argv)
    widget = MyMainWindow()
    widget.show()
    app.exec_()


launch()

1. How to detect that item is double clicked

Connect the function to QTreeWidget.itemDoubleClicked signal.

class MyMainWindow(QMainWindow):
    def __init__(self, parent=None):
        # omitted
    def _generate_ui(self):
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
        self.tree_widget = QTreeWidget()
        self.tree_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.tree_widget.setSelectionBehavior(QAbstractItemView.SelectItems)
        self.tree_widget.itemDoubleClicked.connect(self._doubleClicked) # connect function to the signal itemDoubleClicked
        main_layout.addWidget(self.tree_widget)

    def _init_tree_widget(self):
        # omitted
        
    def _doubleClicked(self): # this function is executed when an item is double clicked
        print("double clicked")

2. How to get double clicked item

Use QTreeWidget.currentItem(), QTreeWidget.currentColumn() or QAbstractItemView.currentIndex().

    def _doubleClicked(self):
        column = self.tree_widget.currentColumn()
        text = self.tree_widget.currentItem().text(column)
        print("double clicked item is " + text)

# output when item1_2 clicked => double clicked item is item1_2
# output when group1 clicked => double clicked item is grounp1

How To Clear Layout in PySide

I’d like to clear and redraw widgets in my custom PySide2 UI.

GOAL

Today’s goal is to remove and delete all children widgets from the layout.

Environment

Windows 10
Python 3.8.7
PySide2 (5.15.2)

Method

I created sample widget with clear-all button.

import sys
from PySide2.QtWidgets import *
from PySide2.QtCore import Qt, QEventLoop

class MyWidget(QWidget):
    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        self.main_layout = QVBoxLayout()
        self.setLayout(self.main_layout)
        self._generateUI()
    def _generateUI(self):
        clear_button = QPushButton("Clear All")
        clear_button.clicked.connect(self._clearall)
        self.main_layout.addWidget(clear_button)
        label = QLabel("Label")
        self.main_layout.addWidget(label)
        combobox = QComboBox()
        combobox.addItems(["comboBox1", "comboBox2"])
        self.main_layout.addWidget(combobox)
        buttons_widget = QWidget()
        buttons_layout = QHBoxLayout()
        buttons_widget.setLayout(buttons_layout)
        for i in range(3):
            button = QPushButton("button"+str(i))
            buttons_layout.addWidget(button)
        self.main_layout.addWidget(buttons_widget)
    
    def _clearall(self): # the function to clear widgets
        pass

if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = MyWidget()
    widget.show()
    sys.exit(app.exec_())

1. Get all children

To get the list of children in the layout, use count() and itemAt(). Because the itemAt() function returns QLayoutItem not Qwidget, we should get widget by using QLayoutItem.widget() method.

    def _clearall(self):
        children = []
        for i in range(self.main_layout.count()):
            child = self.main_layout.itemAt(i).widget()
            if child:
                children.append(child)
                print(child)

# output
# <class 'PySide2.QtWidgets.QPushButton'>
# <class 'PySide2.QtWidgets.QLabel'>
# <class 'PySide2.QtWidgets.QComboBox'>
# <class 'PySide2.QtWidgets.QWidget'>

2. Remove each child item

Use deleteLater() to delete widget.

    def _clearall(self):
        children = []
        for i in range(self.main_layout.count()):
            child = self.main_layout.itemAt(i).widget()
            if child:
                children.append(child)
        for child in children:
            child.deleteLater()

* There are many ways to remove widget from the parent layout. For example, some people use setParent(None) or something. However don’t use removeWidget() because it just removes widget without destroying it. Please refer to the discussion below.

Other Examples

There are another methods to implement _clearall() function.

The method in which items deleted in for loop

It works well because the deleteLater() destroys items after returning control from the function.

    def _clearall(self):
        for i in range(self.main_layout.count()):
            child = self.main_layout.itemAt(i).widget()
            child.widget().deleteLater()

This is a wrong example. The itemAt() function can’t find the child because when an item is deleted with setParent(None), other items will be renumbered.

    def _clearall(self): #wrong example
        for i in range(self.main_layout.count()):
            child = self.main_layout.itemAt(i).widget()
            child.setParent(None)
# An error occurred: AttributeError: 'NoneType' object has no attribute 'widget'

The method by using children() or findChildren() function

You can get children of widget with children() function.

    def _clearall_4(self): #wrong
        for child in self.children():
            print(child.__class__)
            child.deleteLater()
# output
# <class 'PySide2.QtWidgets.QVBoxLayout'> # children() returns layout too
# <class 'PySide2.QtWidgets.QPushButton'>
# <class 'PySide2.QtWidgets.QLabel'>
# <class 'PySide2.QtWidgets.QComboBox'>
# <class 'PySide2.QtWidgets.QWidget'>

You can get children of widget with findChild() function.

    def _clearall_4(self): #wrong
        for child in self.findChildren(QWidget):
            print(child.__class__)
            child.deleteLater()
# output
# <class 'PySide2.QtWidgets.QPushButton'>
# <class 'PySide2.QtWidgets.QLabel'>
# <class 'PySide2.QtWidgets.QComboBox'>
# <class 'PySide2.QtWidgets.QWidget'>
# <class 'PySide2.QtWidgets.QPushButton'>
# <class 'PySide2.QtWidgets.QPushButton'>
# <class 'PySide2.QtWidgets.QPushButton'>

This is a wrong example. The children() of layout returns [].

    def _clearall_4(self): #wrong example
        for child in self.main_layout.children():
            child.deleteLator()
        print("first layout children", self.main_layout.children()) # output => []

How To Create On/Off Button In PySide

GOAL

Today’s goal is to create On/Off button in my PySide UI as below.

Environment

Windows 10
Python 3.8.7
PySide2 (5.15.2)

Method

1. Create a normal QPushButton

import sys
from PySide2.QtWidgets import *

class MyWidget(QWidget):
    def __init__(self):
        super(MyWidget, self).__init__()
        self.layout = QVBoxLayout()
        self.on_off_button = QPushButton("on/off Button")
        self.on_off_button.setCheckable(True)
        #self.layout.addWidget(self.on_off_button)
        self.setLayout(self.layout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = MyWidget()
    widget.show()
    sys.exit(app.exec_())

2. Use setCheckable() method

QAbstractButton has a method setCheckable().

class MyWidget(QWidget):
    def __init__(self):
        super(MyWidget, self).__init__()
        self.layout = QVBoxLayout()
        self.on_off_button = QPushButton("on/off Button")
        self.on_off_button.setCheckable(True) # set Checkable True
        self.layout.addWidget(self.on_off_button)
        self.setLayout(self.layout)

How to change the status, checked or unchecked

To change the status manually, use the setChecked() method of QAbstractButton.

For example, reset button can be implemented by using the function to set the button unchecked.

class MyWidget(QWidget):
    def __init__(self):
        super(MyWidget, self).__init__()
        self.layout = QGridLayout()
        self.reset_button = QPushButton("Reset")
        self.reset_button.clicked.connect(self._reset)
        self.layout.addWidget(self.reset_button,0,0,1,3)

        self.on_off_button1 = QPushButton("on/off 1")
        self.on_off_button1.setCheckable(True)
        self.layout.addWidget(self.on_off_button1,1,0,1,1)

        self.on_off_button2 = QPushButton("on/off 2")
        self.on_off_button2.setCheckable(True)
        self.layout.addWidget(self.on_off_button2,1,1,1,1)

        self.on_off_button3 = QPushButton("on/off 3")
        self.on_off_button3.setCheckable(True)
        self.layout.addWidget(self.on_off_button3,1,2,1,1)
        self.setLayout(self.layout)

    def _reset(self):
        self.on_off_button1.setChecked(False)
        self.on_off_button2.setChecked(False)
        self.on_off_button3.setChecked(False)
When the reset button is clicked, all on/off buttons are set off

How To Get The Class Of Object

GOAL

To get object name from the object.

Environment

Python 3.8.7

Method

Use special attributes “__class__” of the object.

Example

f = open('test.txt', 'r')

# getting class name
print(f.__class__.__name__) 
#output => TextIOWrapper

# getting the module where the class is defined
print(f.__class__.__module__) 
#output => _io

f.close()

special attributes

Reference: Special Attributes

  • object.__dict__
    • A dictionary or other mapping object used to store an object’s (writable) attributes.
  • instance.__class__
    • The class to which a class instance belongs.
  • class.__bases__
    • The class to which a class instance belongs.
  • class.__bases__
    • The tuple of base classes of a class object.
  • definition.__name__
    • The name of the class, function, method, descriptor, or generator instance.
  • definition.__qualname__
    • The qualified name of the class, function, method, descriptor, or generator instance.
  • class.__mro__
    • This attribute is a tuple of classes that are considered when looking for base classes during method resolution.

How To Import Module with Module Name Str in Python

GOAL

To import modules by its name as below.

def import_n_th_module(index):
    """
      the function to get index number and import the module named mymodule+index such as mymodule1, mymodule2,...
    """
    module_name = 'mymodule' + str(index)
    # import module_name  => the error occured

Environment

Python 3.8.7

Method

Use importlib package.

The purpose of the importlib package is two-fold. One is to provide the implementation of the import statement (and thus, by extension, the import() function) in Python source code. <omit>

from importlib — The implementation of import of Python documentaion

You can import module with importlib.import_module() as follows.

importlib.import_module('numpy')
# output => import numpy module

Example

I created 3 modules in modules package. And each module prints “[module_name] is imported” when being imported.

#test.py
import sys
import importlib

index = 3
importlib.import_module('modules.mymodule' + str(index))
# output => "mymodule3 is imported"

Postscript

Another purpose of the imporlib

Two, the components to implement import are exposed in this package, making it easier for users to create their own custom objects (known generically as an importer) to participate in the import process.

from importlib — The implementation of import of Python documentaion

You can see all methods of importlib in importlib — The implementation of import of Python documentaion.

How to get keyconfig object with its path in Blender Python

This is a part of the project “Easy Keymap Generator”.

GOAL

To implement a function that get keyconfig name and return keyconfig object.

The list of keyconfig names can be seen in Preferences window.

Environment

Blender 2.83 (Python 3.7.4)
Windows 10

Method

This is my solution, but I don’t think it is the best way. Please let me know if you have any better ideas.

1 Access bpy.context.window_manager.keyconfigs

def get_keyconfig(keyconfig_name):
    wm = bpy.context.window_manager
    if keyconfig_name in wm.keyconfigs.keys():
        return wm.keyconfigs[keyconfig_name]
    else:
        return None

2 Activate specified keyconfig and return active keyconfig

If there is no keyconfig you’d like to get in current environment, you should activate the keyconfig by its path manually.

key_config_dict = get_keyconfig_dict()
configs = list(key_config_dict.keys())
print(configs) 
# output => ['blender', 'blender_27x', 'industry_compatible', 'test1', 'test2']

kc = get_keyconfig(configs[1], key_config_dict)
print(kc) 
# output => blender_27x <bpy_struct, KeyConfig("blender_27x")>

2.1 Get keyconfigs and paths to the keyconfig file.

I created a function to get the dict with keys of keyconfig name and values of path to the keyconfig python file.
Reference: How to get key config list in Blender Python

def get_keyconfigs():
    """
    :return: dict{kerconfig_name(str): path to config file(str)}
    """
    config_pathes = bpy.utils.preset_paths("keyconfig")
    config_dict = {}
    for config_path in config_pathes:
        for file in os.listdir(config_path):
            name, ext = os.path.splitext(file)
            if ext.lower() in [".py", ".xml"] and not name[0] == ".":
                config_dict[name] = os.path.join(config_path, file)
    return config_dict

2.2 Activate specified keyconfig and return active keyconfig

Activate the specified keyconfig by its path and get active keyconfig with wm.keyconfigs.active then restore active keyconfig to original settings.

def get_keyconfig(keyconfig_name, keyconfig_dict):
    wm = bpy.context.window_manager
    if keyconfig_name in wm.keyconfigs.keys(): #if the keyconfig can be found, return it
        return wm.keyconfigs[keyconfig_name]
    else: # activate by config path and return it
        keyconfig_path = keyconfig_dict[keyconfig_name]
        current_path = keyconfig_dict[wm.keyconfigs.active.name]
        bpy.ops.preferences.keyconfig_activate(filepath=keyconfig_path)
        kc = wm.keyconfigs.active
        bpy.ops.preferences.keyconfig_activate(filepath=current_path)
        return kc

How to get key config list in Blender Python

This is a part of the project “Easy Keymap Generator”.

GOAL

To get the key config list in my addon. The list of key configs can be seen in Preferences window.

Environment

Blender 2.83 (Python 3.7.4)
Windows 10

Method

Get the list directly from the preset directories.
Related article: How to get preset paths in Blender Python

def get_keyconfigs():
    """
    :return: dict{kerconfig_name(str): path to config file(str)}
    """
    config_pathes = bpy.utils.preset_paths("keyconfig")
    config_dict = {}
    for config_path in config_pathes:
        for file in os.listdir(config_path):
            name, ext = os.path.splitext(file)
            if ext.lower() in [".py", ".xml"] and not name[0] == ".":
                config_dict[name] = os.path.join(config_path, file)
    return config_dict

print(key_config_dict.keys())
#output => dict_keys(['blender', 'blender_27x', 'industry_compatible', 'test1', 'test2'])

How to get preset paths in Blender Python

This is a tips for blender addon developers.

GOAL

To get the list of paths to preset directories, blender system preset and user preset and so on.

These are examples of the preset directory.

  • presets of interface theme
    • C:\Program Files\Blender Foundation\Blender 2.83\2.83\scripts\presets\interface_theme
    • C:\Program Files\Blender Foundation\Blender 2.83\2.83\scripts\addons\presets\interface_theme’
  • presets of keyconfig
    • C:\Program Files\Blender Foundation\Blender 2.83\2.83\scripts\presets\keyconfig
    • C:\Users\<USER_NAME>\AppData\Roaming\Blender Foundation\Blender\2.83\scripts\presets\keyconfig

Environment

Blender 2.83 (Python 3.7.4)
Windows 10

Method

Use bpy.utils.preset_paths(subdir) that returns the list of paths to the preset directory of subdir.

(more…)

How to remove widget from the layout in PySide

I’d like to add close button to the single widget and remove the widget from the parent layout when the close button pressed.

GOAL

To remove single widget with close button as below.

Environment

Windows 10
Python 3.8.7
PySide2 5.15.2

Method

1. Create a widget with close button and set it in the layout

I create ItemWidget with close button and put the item and add button on the main window. Check “How To Insert Widget To The Layout Counting From The End” for detail about the implement of add button.

import sys
from PySide2.QtWidgets import *

class ItemWidget(QWidget):
    def __init__(self, id_str="", parent=None):
        super(ItemWidget, self).__init__(parent)
        self.id_str = id_str
        self._generateUI()
    def _generateUI(self):
        main_layout = QGridLayout()
        self.setLayout(main_layout)
        title = QLabel("title" + self.id_str)
        main_layout.addWidget(title, 0, 0, 1, 3)
        close_button = QPushButton("-")
        close_button.setFixedWidth(30)
        main_layout.addWidget(close_button, 0, 3, 1, 1)
        spinbox = QSpinBox()
        main_layout.addWidget(spinbox, 1, 0, 1, 4)

class MyMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        self.id_num = 1
        self._generateUI()
    def _generateUI(self):
        main_widget = QWidget()
        self.main_layout = QVBoxLayout()
        main_widget.setLayout(self.main_layout)
        self.setCentralWidget(main_widget)
        item = ItemWidget(str(self.id_num))
        self.main_layout.addWidget(item)
        add_button = QPushButton("+")
        add_button.clicked.connect(self._addItem)
        self.main_layout.addWidget(add_button)
    def _addItem(self):
        self.id_num += 1
        item = ItemWidget(str(self.id_num))
        self.main_layout.insertWidget(self.main_layout.count()-1, item)

def launch():
    app = QApplication.instance()
    if not app:
        app = QApplication(sys.argv)
    widget = MyMainWindow()
    widget.show()
    app.exec_()

launch()

2. Connect close function to the signal “clicked” of the close button

Use deleteLater() to delete widget.

class ItemWidget(QWidget):
    def __init__(self, id_str="", parent=None):
        super(ItemWidget, self).__init__(parent)
        self.id_str = id_str
        self._generateUI()
    def _generateUI(self):
        main_layout = QGridLayout()
        self.setLayout(main_layout)
        title = QLabel("title" + self.id_str)
        main_layout.addWidget(title, 0, 0, 1, 3)
        close_button = QPushButton("-")
        close_button.setFixedWidth(30)
        close_button.clicked.connect(self._close_widget) # add to close the widget
        main_layout.addWidget(close_button, 0, 3, 1, 1)
        spinbox = QSpinBox()
        main_layout.addWidget(spinbox, 1, 0, 1, 4)
    def _close_widget(self):
        self.deleteLater() # main function to close widget

* There are many ways to remove widget from the parent layout. For example, some people use setParent(None) or something. However don’t use removeWidget() because it just removes widget without destroying it. Please refer to the discussion below.

3. Resize the parent widget when the child removed

The function deleteLater() just delete the widget without changing the size. So I changed size manually with adjustSize() as below.
Related article: How to detect the child widget destroy?

class MyMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        self.id_num = 1
        self._generateUI()
    def _generateUI(self):
        main_widget = QWidget()
        self.main_layout = QVBoxLayout()
        main_widget.setLayout(self.main_layout)
        self.setCentralWidget(main_widget)
        item = ItemWidget(str(self.id_num))
        self.main_layout.addWidget(item)
        add_button = QPushButton("+")
        add_button.clicked.connect(self._addItem)
        self.main_layout.addWidget(add_button)
    def _addItem(self):
        self.id_num += 1
        item = ItemWidget(str(self.id_num))
        self.main_layout.insertWidget(self.main_layout.count()-1, item)
    def _deletedItem(self, item):
        self.id_num -= 1
        for i in range(3):
            QApplication.processEvents()
        self.adjustSize()

I don’t know why but adjustSize() doesn’t work correctly until some events are processed in the event loop. So I called QApplication.processEvents() in the for loop. Please let me know if you have any idea about this

Reference: PySide: What is the best way to resize the main window if one widget is hidden? in stackoverflow.com