top of page
Qt in houdini

Lesson 2 : UI in Houdini

In this course, you will learn how to create your own interface usable in Houdini and other software.

This will enable you to save time and create your own tools, making the script work without the need to rely on commands for execution.

Table of Contents
 

1. Why create your own user interface

2. Install and use Qt 

3. Create elements in Qt

4. Retrieve/execute information with the interface

5. Practical case in houdini

6. Sources

1. Why create your own user interface

Qt in houdini

By developing your own UI, you can design a user experience that precisely matches your application's needs. You can customize the appearance, behavior, and features to specifically meet your users' expectations.

​

​

​

Using tools like Qt and PyQt, you have control over every visual

and functional aspect of your application. This enables you to

create an intuitive and user-friendly interface, ensuring an optimal

user experience.

​

​

Creating a customized UI allows you to ensure it seamlessly integrates

with the specific functionalities of your application. This promotes a

consistent and harmonious user experience.

​

​

In essence, creating your own user interface allows you to provide a unique experience tailored to your application's specific needs, offering total control over the user experience and strengthening your product's identity.

In order to create our own normal user interface, we used QT. QT is a popular cross-platform library for developing Graphical User Interfaces (GUIs) in Python. With Qt, you can create applications with an attractive and functional user interface.

​

Qt is a C++ framework developed by the Qt Company. It provides a variety of tools and components for creating graphical interfaces, mobile applications, embedded applications, and more. PyQt and PySide are two Python wrappers for the Qt library, enabling the use of Qt with Python.

1. installer and use Qt 

To program our interface, we used an IDE, and the one we're going to use is Visual Studio Code.

We've already used it, and it's absolutely perfect for creating our application. It has the Maya library, and we'll be able to install QT in Visual Studio Code. With all of this, it offers simplicity in use."

visual studio code

What you need to know is that the entire interface of Houdini has been created with Qt, and in Houdini, we have direct access to the Pyside2 binding (associated with Qt5), which is available in Houdini without the need for installation. So, to create our interface in Houdini, we will use Pyside2. You can use other versions of PySide, like PySide6, if you prefer, but you'll need to install this new version in Houdini.

To use Qt in Visual Studio Code, you'll need to install the PySide2 library, which is the Python binding for Qt5.

​

To do this, you need to go to the Visual Studio Code terminal and write the following bash command:

"pip install PySide2" using pip. To understand the difference between QT, PySide, and PyQt, I recommend visiting the website KooR.fr."

pyside Qt interface

From there, you can start creating your first Python scripts that will build an interface using PySide.

​

The first thing to do is to call the library and create your first window by creating an application.

​

For this to work, you need to create an application that sets up an entire environment allowing the interface to function within its own loop without interfering with other software. Within this application, we can create our window and inside it, we can do absolutely anything we want.

pyside Qt interface

import sys

import PySide2.QtWidgets as QT


 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first window Qt')

 

    return my_window 
 

if __name__ == '__main__':

    app = QT.QApplication(sys.argv)

    my_window = main_intereface()

    my_window.show()

    sys.exit(app.exec_())

  • QApplication: This is the main class for a Qt application. It manages the main event loop and initializes the application.

​

  • QWidget: It's a fundamental graphical interface element that provides a window or framework for applications.

​

  • show(): This method displays the window on the screen. It needs to be executed outside of the main window method.

​

  • app.exec_(): This is an event loop that waits for user interactions.

​

  • sys.argv: It's a list of command-line arguments passed to the Python script. This allows Qt to process any specified arguments when launching the application.

3. Create elements in Qt

Qt offers various user interface elements such as buttons, text fields, menus, etc. This allows you to have interfaces as you desire, you can literally recreate Maya.

​

Here are some key elements when creating an interface:

  • QPushButton() : creates a button in your window.

  • QLabel() : creates non-editable text, it's a static text.

  • QLineEdit() : creates an input box where the user can write.

  • QCheckBox() : allows you to create checkboxes.

  • QT.QSpinBox() : enables you to create a numerical counter.

​

pyside Qt interface

import sys

import PySide2.QtWidgets as QT


 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first window Qt')

 

    #----create element-----

    QT.QPushButton()

    QT.QLabel()

    QT.QCheckBox()

    QT.QSpinBox()

​

    return my_window 
 

if __name__ == '__main__':

    app = QT.QApplication(sys.argv)

    my_window = main_intereface()

    my_window.show()

    sys.exit(app.exec_())

If we run the program with its different elements, we might notice that they are not displayed in the window. Indeed, although we have created our elements and they do exist, they aren't connected to anything; they are not inside a box.

​

What you need to know is that Qt works with a system of box stacking. This means that one box will contain other boxes, potentially nested within other boxes. This allows us to create our hierarchy.

​

To achieve this, we'll need to pass as an argument the variable of the QWidget, which is our main window. For each element created, as the first argument, we'll put the variable "my_window". This way, each element will be a child of and belong to the window "my_window".

​

pyside Qt interface

import sys

import PySide2.QtWidgets as QT


 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first window Qt')

 

    #----create element-----

    QT.QPushButton(my_window)

    QT.QLabel(my_window)

    QT.QCheckBox(my_window)

    QT.QSpinBox(my_window)

​

    return my_window 
 

if __name__ == '__main__':

    app = QT.QApplication(sys.argv)

    my_window = main_intereface()

    my_window.show()

    sys.exit(app.exec_())

Now you have the possibility to see these different elements, but they are overlapping and have no name or indication. For example, we would like there to be text on the button, and the label to have text as currently, it is blank. We'd also like to know what the checkbox corresponds to, perhaps with a small phrase explaining what we're checking.

​

To achieve this, we'll need to use methods that modify the placement of the different elements, allowing us to add text to these elements and much more. To do this, we'll have to store each element we've created in a variable, and from there, we'll have access to a multitude of methods provided by these elements (for those more familiar, each element created with tools is a class buttons, labels, checkboxes are classes).

​

The methods we'll be using are as follows:

  • setGeometry(): This method allows us to place an element exactly where we want it in pixels. It includes four arguments (X, Y, XH, YH). The first two determine the starting position of the element on the X and Y axes, respectively.  XH defines the width the element will take on the X-axis, and the last defines the height the element will take on the Y-axis.

​

  • setText(): This method allows us to assign a name or text to the respective element. It takes one argument, which is the text you want to assign.

pyside Qt interface

#----create element-----

var_Button = QT.QPushButton(my_window)

var_Button.setText('I am a Button')

var_Button.setGeometry(0, 10, 100, 40)

​

var_Label = QT.QLabel(my_window)

var_Label.setText('I am a Label')

var_Label.setGeometry(0, 100, 100, 40)

​

var_Checker = QT.QCheckBox(my_window)

var_Checker.setText('I am a CheckBox')

var_Checker.setGeometry(0, 200, 100, 40)

​

var_SpinBox = QT.QSpinBox(my_window) #The text cannot be placed on this element

var_SpinBox.setGeometry(0, 300, 100, 40)

​

return my_window 

A quick tip: if you're using the setText() method, there's a faster way to do it by directly inputting the text as an argument in the elements you want to create.

#----create element-----

var_Button = QT.QPushButton('I am a Button'my_window)

var_Button.setGeometry(0, 10, 100, 40)

​

var_Label = QT.QLabel('I am a Label'my_window)

var_Label.setGeometry(0, 100, 100, 40)

​

var_Checker = QT.QCheckBox('I am a CheckBox'my_window)

var_Checker.setGeometry(0, 200, 100, 40)

​

var_SpinBox = QT.QSpinBox(my_window) #The text cannot be placed on this element

var_SpinBox.setGeometry(0, 300, 100, 40)

​

return my_window 

The geometry method is very useful for positioning an element exactly where you want it. However, it can be redundant and quickly become inconvenient because the element will have a fixed position. For instance, if we resize the window, the elements won't adapt to the window's size.

pyside Qt interface

To address this, I recommend using layouts. Layouts are boxes that allow you to place all the elements inside them, organizing and arranging themselves automatically without you needing to do it manually. There are several primary Layouts:

  • QT.QBoxLayout() : This is an abstract class that arranges widgets either horizontally or vertically within a box. It serves as the base class for QHBoxLayout and QVBoxLayout.

​

  • QT.QFormLayout() : This layout is designed for creating form-based layouts typically used for form-like user interfaces. It arranges widgets in rows, usually label-widget pairs.

​

  • QT.QGridLayout() : This layout arranges widgets in a grid. Widgets are placed in cells defined by their row and column positions. It allows for precise positioning of widgets in a grid-like structure.

​

  • QT.QHBoxLayout() : This arranges widgets in a horizontal row, placing them side by side.

​

  • QT.QVBoxLayout() : This arranges widgets in a vertical column, stacking them on top of each other.

​

​

I recommend primarily using QHBoxLayout or QVBoxLayout layouts. Here's an example using both layouts.

The only downside with layouts is that you can no longer use "my_window" as an argument in the various elements because this argument is window-based and doesn't function with layouts. Therefore, if you want to add an element to a layout, you'll need to use the addWidget() method and pass the element you want to add as an argument. You'll need to do this for each element.

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)#my_window is QWidget, can use it an argument

​

var_Button = QT.QPushButton('I am a Button')#layout_vertical not QWidget, can't use it an argument

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox() #The text cannot be placed on this element

layout_vertical.addWidget(var_SpinBox)

​

return my_window 

pyside Qt interface
pyside Qt interface

QVBoxLayout()                                                                        QHBoxLayout()

Using this method system, you have the ability to customize the various elements as you desire. For instance, with setStyleSheet(), you can change the font, text size, have rounded edges on buttons, alter text and background colors, modify the selection color, change the cursor appearance, and more. There's a wide array of elements you can adjust.

​

For example, to delve even deeper into customizing the various elements, you can do this:

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)#my_window is QWidget, can use it an argument

​

var_Button = QT.QPushButton('I am a Button')#layout_vertical not QWidget, can't use it an argument

var_Button.setStyleSheet("""background-color: rgb(10, 50, 80); 

                                color: #ffffff;

                                font-size: 80px;""")

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox() #The text cannot be placed on this element

layout_vertical.addWidget(var_SpinBox)

​

return my_window 

For those more knowledgeable in programming, you might have recognized a bit of the CSS structure. Indeed, Qt uses something quite similar to style its interfaces, it uses a language called QSS, specific to Qt but incorporating the same functionalities and foundations as CSS.

​

It's possible to separate the styling of your interface from the interface itself. What I mean by this is that if you want to style your interface, you need to use the setStyleSheet() method for each created element. This can take up a lot of space in our code because in my example, there are only three lines, but you could end up with around twenty or thirty lines just for one element. If you repeat this operation every time for each element, the code will quickly become unreadable.

​

There's a way to address this, similar to HTML and CSS, by using another file type, QSS, to contain all the styling for your page. This allows you to separate the visual aspect from the interface aspect.

pyside Qt interface

For a simple and basic initial interface, there's not much need to use a QSS file. However, if you intend to create a fairly substantial interface with many functionalities and numerous elements, I recommend visiting this website Koor.fr to learn how to use QSS.

4. Retrieve/execute information with the interface

From here, you've created your first styled elements and positioned them where you want. Now, it's time to understand how to use buttons, retrieve information, for instance, from a checkbox or a live edit, and execute a function when a button is pressed.

​

Let's start with understanding how to execute a piece of code when clicking a button. Qt uses a system of connection between the function you want to call and the button. You establish a connection to the method to trigger its execution.

pyside Qt interface

And to do that, we'll simply create a function that will print("hello") and establish the connection between the button and the function. Firstly, we'll use the "clicked" method, which sends a signal when the button is clicked. Then, we'll use the "connect" method to link the signal emission to a specific function.

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)

​

var_Button = QT.QPushButton('I am a Button')

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox()

layout_vertical.addWidget(var_SpinBox)

​

#----connection button----

var_Button.clicked.connect(myfuncButton)#emit signal and connect the signal to the function

​

return my_window 

function for the button when is pressed

def myfuncButton():

    print('hellow Qt')

Now what we'd like to do is to retrieve information from the interface, for example, to know if the checkbox is checked or not, to retrieve the content from the lineEdit, and to gather these different pieces of information.

​

To do this, it's quite simple, we just need to use methods like value() / isChecked() / text() / etc. Using these methods for any element, we can retrieve its value. For instance, if we want to retrieve the value from a QSpinBox and a checkBox, we would do something like this.

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)

​

var_Button = QT.QPushButton('I am a Button')

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox()

layout_vertical.addWidget(var_SpinBox)

​

#----connection button----

var_Button.clicked.connect(myfuncButton)#emit signal and connect the signal to the function

 

#----print value-----

print(var_Checker.isChecked())

print(var_SpinBox.value())

​

return my_window 

Thanks to this, you can see that we retrieve the values of the elements. However, as we execute our code, we retrieve these values at the current moment, which is when the interface is created. What we actually want is that when we click the button, for example, we want to retrieve these values.

 

For this, we'll need to pass the variables "var_Checker" and "var_SpinBox" as arguments into the "myfuncButton()" function.

​

And in the line to connect the function to the button, we need to slightly change the "Connect" method. Instead of using .connect(myfuncButton), we'll use .connect(lambda: myfuncButton(var_Checker, var_SpinBox))

​

Now that our function takes arguments into account, we need to pass these arguments in the connect(). We also use lambda because if we don't, as soon as the interface is created, the function will execute directly, which is not what we want; we want it to execute only when we press the button.

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)

​

var_Button = QT.QPushButton('I am a Button')

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox()

layout_vertical.addWidget(var_SpinBox)

​

#----connection button----

var_Button.clicked.connect(lambda : myfuncButton(var_Checker, var_SpinBox))

​

return my_window 

function for the button when is pressed

def myfuncButton(var_Checker, var_SpinBox)

    print(var_Checker.isChecked())

    print(var_SpinBox.value())

Now you're able to retrieve the various values and information that the user can input into the interface and use them as needed.

​

Considering we can retrieve information from the interface, we can also do the opposite, we can put information on the interface when the user does something. For instance, we'd like that when the user presses the button and if the checkbox is checked, change the text of the QLabel to 'is Checked', and if the checkbox isn't checked, replace the text of the QLabel with "not checked".

​

To achieve this, we simply need to add the QLabel into the arguments of the "myfuncButton" function and the "connect()". Then, we just need to use the "setText()method to change the text of the QLabel.

pyside Qt interface

#----create element-----

layout_vertical = QT.QVBoxLayout(my_window)

​

var_Button = QT.QPushButton('I am a Button')

layout_vertical.addWidget(var_Button)

​

var_Label = QT.QLabel('I am a Label')

layout_vertical.addWidget(var_Label)

​

var_Checker = QT.QCheckBox('I am a CheckBox')

layout_vertical.addWidget(var_Checker)

​

var_SpinBox = QT.QSpinBox()

layout_vertical.addWidget(var_SpinBox)

​

#----connection button----

var_Button.clicked.connect(lambda : myfuncButton(var_Checker, var_SpinBox, var_Label ))

​

return my_window 

function for the button when is pressed

def myfuncButton(var_Checker, var_SpinBox, var_Label):

    print(var_Checker.isChecked())

    print(var_SpinBox.value())

​

    if var_Checker.isChecked():

        var_Label.setText('is Checked')

    else:

        var_Label.setText('not Checked')

Thanks to these various methods or others, you can retrieve all the different elements you want, modify them, and do whatever you want with them. For the sake of simplicity and efficiency, I recommend creating your interface in the form of a Python class so that it's easier to use and more conventional.

​

Here's an example of what the code could look like as a class.

pyside Qt interface

5. Practical case in Houdini

To materialize all this, we will see an example in Houdini. We want to create an interface that allows us to create sub-networks, give them names, choose whether they should be hierarchical, and specify the number we want to create.

​

First, we need to create our interface, which will include a button to execute the program, a lineEdit to choose the names of the sub-networks, a checkbox to determine if the sub-networks should be hierarchical or not, and a spinbox to choose the number of subnetworks to create. Finally, we will organize all of this within a layout.

​

So, initially, we will have a code that looks like this, with an application that creates a window.

pyside Qt interface

import sys

import PySide2.QtWidgets as QT


 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first UI in Houdini ')

 

    return my_window 

​

app = QT.QApplication(sys.argv)

my_window = main_intereface()

my_window.show()

sys.exit(app.exec_())

"if __name__ == '__main__': " cette ligne ne fonctionnera pas dans houdini il faudra l'enlever.

But be careful, if you run this program in Houdini, you will encounter a nice Fatal Error message. Indeed, every time you run your window, Houdini will literally crash. Why? Because what you need to know is that, as Houdini already has a running application, in your interface, you are trying to create a new application within an existing one. This will literally cause Houdini to crash every time because it is not possible to create an application within another application.

crash hounidi

To fix this, we'll modify the application so that it can retrieve the instance of the existing application, which is houdini in this case, allowing our window to attach itself to the houdini application. We'll replace QT.QApplication(sys.argv) with QT.QApplication.instance() and we can remove the line sys.exit(app.exec_()). This is because, by retrieving the instance of the application, we don't need to launch it separately as it's already running.

​

QT.QApplication.instance() in PyQt/PySide is used to retrieve the instance of the QApplication object that represents the running application.

It returns a reference to the currently running QApplication instance. This method is typically used to get access to the application instance from anywhere within the code, allowing interactions or modifications to the application properties and behavior. For instance, it can be used to access settings, manage the application's event loop, or perform other operations related to the application's state. If there's no QApplication instance running, it returns None.

​

In this case, we no longer need the sys library, so we can remove it.

import PySide2.QtWidgets as QT


 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first UI in Houdini ')

​

    return my_window 

​

app = QT.QApplication.instance()

my_window = main_intereface()

my_window.show()

pyside Qt interface

A quick tip if you want your program to detect whether it needs to create an application or retrieve the instance of the open application. This allows you to run your code on Visual Studio (where it needs to create an application to work) and Houdini (where it needs to retrieve the Houdini application instance to function).

​

We'll need to use a conditional statement "if" to determine whether it should create or use the application instance.

import PySide2.QtWidgets as QT

import sys
 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first UI in Houdini ')

 

    return my_window 

if not QT.QApplication.instance():

    app_start =

    app = QT.QApplication(sys.argv) #if no apllication executed create aplication

else:

    app_start = 0

    app = QT.QApplication.instance() #if apllication executed create intance

my_window = main_intereface()

my_window.show()

if app_start: # if we create app start the app 

    app.exec_()

pyside Qt interface

Now that we have this, we can start putting together the various elements to have our final interface.

import PySide2.QtWidgets as QT

import sys
 

def main_intereface():

    my_window = QT.QWidget()

    my_window.resize(500, 440)

    my_window.setWindowTitle('my first UI in Houdini ')

​

    my_layout = QT.QVBoxLayout(my_window)

   

    name_subNet = QT.QLineEdit('name subNet')

    my_layout.addWidget(name_subNet)

   

    hierarchy= QT.QCheckBox('Hierarchy')

    my_layout.addWidget(hierarchy)

   

    nmb_subNet = QT.QSpinBox()

    my_layout.addWidget(nmb_subNet)

   

    button = QT.QPushButton('create subNet')

    my_layout.addWidget(button)

    return my_window 
 

if not QT.QApplication.instance():

    app_start =

    app = QT.QApplication(sys.argv) #if no apllication executed create aplication

else:

    app_start = 0

    app = QT.QApplication.instance() #if apllication executed create intance

my_window = main_intereface()

my_window.show()

if app_start: # if we create app start the app 

    app.exec_()

pyside Qt interface

Now that we have that, we can start putting together the various elements to create our final interface. We will begin by writing the function that will allow us to create sub-networks in Houdini. For this, we will use the 'import hou' from Houdini. Firstly, we will create the root, the place where the nodes are created. Then, based on the number of iterations, we create the node with our name. If the checkbox is activated, the root will change, creating the node. If the checkbox is not activated, we change the position of the subnet. It will look like this (don't forget to import the 'hou' library into your code: 'import hou').

def create_subNetWork(name_subNet, nmb_subNet, hierarchy):

    where = hou.node("/obj/geo1")

    for i in range(nmb_subNet.value()):

        node_subNet = where.createNode("subnet", name_subNet.text())

       

        if hierarchy.isChecked():

            where = node_subNet

        else:

            node_subNet.setPosition([2*i, 0])

  1. def create_subNetwork(name_subNet, nmb_subNet, hierarchy):
        # This is a function named create_subNetwork taking three parameters: name_subNet, nmb_subNet, and hierarchy.
        # This function creates sub-networks based on the provided parameters.

  2.     where = hou.node("/obj/geo1")
        # This sets the variable 'where' to represent the "geo1" type node in Houdini's object tree.
        # This is the node where the sub-networks will be created.

  3.     for i in range(nmb_subNet.value()):
            # This is a loop that iterates 'nmb_subNet' times, where 'nmb_subNet' is a variable indicating how many sub-networks need to be created.

  4.         node_subNet = where.createNode("subnet", name_subNet.text())
            # At each iteration, a new "subnet" type node is created under the 'where' node (initially set to /obj/geo1).
            # The name of the sub-network is determined by 'name_subNet.text()'.

  5.         if hierarchy.isChecked():
                # This checks if the "hierarchy" checkbox is checked.
                # If so, the 'where' node is updated to be equal to the newly created sub-network node.
                # This means that the next sub-networks will be created inside the currently created sub-network.

  6.         else:
                # If the "hierarchy" checkbox is not checked, it means that the sub-networks should be created in a row, not inside each other.
                # In this case, the sub-networks are positioned horizontally side by side.

  7.             node_subNet.setPosition([2 * i, 0])
                # If hierarchy is not selected, this places the newly created sub-networks side by side horizontally,
                # with a position offset of 2 units on the x-axis for each sub-network.

​

Now, all that's left is to connect the button to the function, and everything will work. You will need to use the same method as we used previously with the button to make it work, using clicked.connect().

button.clicked.connect(lambda :create_subNetWork(name_subNet, nmb_subNet, hierarchy))

pyside Qt interface

Here's a small tip: if you want the window to always stay on top, use this method on "my_window". 

"my_window.setWindowFlags(my_window.windowFlags() | Qt.WindowStaysOnTopHint)"

6. Conclusion

Well done! Now you have the ability to create your own interface, connect your functions to an interface, and establish communication between your interface and scripts that will interact in any software.

 

Thanks to this, you have the ability to do absolutely anything that comes to your mind, from creating a program to generating shortcuts for yourself, to even creating colorful interfaces that blink everywhere like a Christmas tree.

bottom of page