Qt for Windows on ARM

While the use of an ARM-based platform on desktops quickly became the "next big thing" in the macOS world a while ago, the situation in the Microsoft Windows ecosystem is a bit different. One of the values of the Windows platform is a long retention of established architectures. Due to this, an adoption of a "new" architecture is slower on Windows. Even though Windows on ARM provides emulation for running x64 binaries, there is a performance cost for this. Some of our users ask to provide native support for Windows on ARM (WoA). In this blogpost, you will learn what is available today and get insights on where we want to go.

IPR and Using AI in Product Creation

Software development has come a long way in the past few decades. With technological advancements, the use of machine learning and artificial intelligence (AI) in software development tools has become increasingly common. AI has the potential to revolutionize the way software is developed, making it faster, more efficient, and less prone to errors. However, as with any new technology, there are important considerations that must be taken into account when using AI in product and software creation. A key consideration is the impact on intellectual property rights (IPR), especially for the ownership and licensing of AI-generated outputs.

Your First Steps With the Kivy Library for GUI Development

Kivy is an open-source Python software library for developing graphical user interfaces. It supports cross-platform development for the desktop as well as the creation of multi-touch apps for mobile devices.

Kivy apps can run across several platforms, including Windows, Linux, macOS, Android, and IOS. One place where Kivy particularly shines is in game development. By combining Kivy's 2D physics capabilities with a simple physics engine, you can create impressive 2D simulations and games.

In this article, you'll learn about using Kivy to develop Python apps. We will go through an introduction to Kivy and what it can do. You'll learn how to create a simple Kivy app in Python and learn the basics of Kivy's styling language, known as Kv. Finally, you'll use Kivy's graphics module to draw 2D shapes on the Kivy canvas.

To get the most out of this tutorial, you should have basic knowledge of Python. Previous knowledge of general concepts of GUI programming, such as event loops, widgets, layouts, and forms, is also a plus.

There are many different Python GUI libraries available, and choosing one for your project can be a really tough and confusing decision to make. For advice see our guide to Python GUI libraries.

Let's get started. We'll first take a few moments to install and set up Kivy on your computer.

Installing Kivy

Before using a third-party library like Kivy, we must install it in our working environment. Installing Kivy is as quick as running the python -m pip install kivy command on your terminal or command line. This command will install the library from the Python package index (PyPI).

Note that as of the time of writing this tutorial, Kivy only officially supports Python versions up to 3.10. For detailed information about installing Kivy, visit the official installation guide.

However, when working with third-party libraries, it's good practice to create a Python virtual environment, which is a self-contained Python installation for a particular version of Python that you can use to isolate the dependencies of a given project.

To create a virtual environment, you'll typically use Python's venv module from the standard library. Fire up a command-line window and type in the following command in your working directory.

sh
$ python -m venv kivy_env

This command will create a folder called kivy_env containing a Python virtual environment. The Python version in this environment is the same as you get when you run python --version on your command line.

Next, we need to activate the virtual environment. Use the appropriate command, depending on whether you're on Windows, macOS, or Linux:

sh
C: /> .\kivy_env\Scripts\activate

sh
$ source kivy_env/bin/activate

sh
$ source kivy_env/bin/activate

Once that's confirmed to be working, you can then install Kivy within the virtual environment you just created by running the following:

sh
(kivy_env) $ python -m pip install kivy

With this command, you'll install Kivy in your active Python virtual environment, so you're now ready to go.

You can also install Kivy by downloading its source code directly from GitHub and doing a manual installation on the command line. For more information on following this installation path, check out the section about installing Kivy from source in the documentation.

Writing Your First Kivy GUI App in Python

Without further ado, let's get right into creating our first app with Kivy and Python. For this app, we will use a Label object to display the traditional "Hello, World!" message on our screen. To write a minimal Kivy GUI app, we need to run a few steps:

  1. Subclassing the App class
  2. Implementing its build() method, which returns a Widget instance
  3. Instantiating this class and calling its run() method

Let's start by importing the required classes. For our example, we only need the App and Label classes. Create a Python file called app.py and add the following imports:

python
from kivy.app import App
from kivy.uix.label import Label

The App class provides the base functionality required to create GUI apps with Kivy, such as managing the event loop. Meanwhile, the Label class will work as the root visual element or widget for our GUI.

Next, we can create our subclass of App. We have called it MainApp here. However, you can call it whatever you like:

python
from kivy.app import App
from kivy.uix.label import Label

class MainApp(App):
    def build(self):
        return Label(text="Hello, World!")

This subclass uses the concept of inheritance in object-oriented programming (OOP) in Python. All the attributes and methods defined in the superclass, App, are automatically inherited by the subclass, MainApp.

In order for our app to create a UI, we need to define a build() method. In build(), we create and return either a widget or layout, which will be the root object in our UI structure.

The build() method is the entry point to whatever will be drawn on the screen. In our example, it creates and returns a label with the "Hello, World!" text on it.

Finally, we need to create an instance of MainApp and call its run() method:

python
from kivy.app import App
from kivy.uix.label import Label

class MainApp(App):
    def build(self):
        return Label(text="Hello, World!")

MainApp().run()

In the final line, we create an instance of MainApp and call its run() method. This method launches the application and runs its main loop. That's it! We're ready to run our first Kivy app. Open your command line and run the following command:

sh
$ python app.py

You'll see the following window on your screen:

First Kivy GUI Application First Kivy GUI Application

Great! You've just written your first Kivy GUI app using Python. It shows a black window with the message "Hello, World!" In its center. Note that the window's title bar shows the title Main, which comes from the name of your App subclass.

The next step is to explore some other essential features of Kivy that will allow you to write fully-functional GUI apps with this library.

Exploring Widgets and Layouts

In the previous section, we mentioned widgets and layouts a few times -- you may be wondering what they are! A widget is an element of a GUI that displays information or provides a specific function. They allow your users to interact with your app's GUI.

A layout, on the other hand, provides a way of arranging widgets into a particular structure in your application's windows. A layout can also give certain behaviors to widgets that belong to it, like the ScatterLayout, which enables multi-touch resizing of a child widget.

In Kivy, you'll find widget and layout classes in their corresponding module under the kivy.uix module. For example, to import the Button class, we can use:

python
from kivy.uix.button import Button

In Kivy, widgets and layout classes are usually located in modules named after the class itself. However, the class uses CamelCase, and the containing module uses lower casing.

For example, take the following imports:

python
# Widgets
from kivy.uix.label import Label
from kivy.uix.image import Image

# Layouts
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout

You'll find some exceptions to this naming convention. For example:

python
from kivy.uix.image import AsyncImage
from kivy.uix.screenmanager import FadeTransition

This commonly happens with modules that define multiple and closely related classes, such as Image and AsyncImage.

Widgets

Widgets are the building blocks of Kivy-based GUIs. Some of the most commonly used GUI widgets in Kivy apps include the following:

  • Widget is the base class required for creating widgets.
  • Label is used for rendering text on windows and dialogs.
  • TextInput provides a box for editable plain text.
  • Button triggers actions when the user presses it.
  • CheckBox provides a two-state button that can be either checked or unchecked.
  • Image is used to display an image on your GUIs.
  • ProgressBar visualizes the progress of some tasks.
  • DropDown provides a versatile drop-down list that can list different widgets.

With these widgets and some others that Kivy provides, you can build complex and user-friendly interfaces for your applications.

Layouts

Kivy also has a rich set of layout classes that allows you to arrange your widgets coherently and functionally to build up GUIs. Some examples of common layouts include:

  • BoxLayout arranges widgets sequentially in either a vertical or horizontal fashion.
  • FloatLayout arranges widgets in a specific position on the containing window.
  • RelativeLayout arranges child widgets according to relative positions.
  • GridLayout arranges widgets in a grid defined by the rows and columns.
  • PageLayout creates multi-page layouts in a way that allows flipping from one page to another.
  • ScatterLayout positions its child widgets similarly to a RelativeLayout.
  • StackLayout stacks in a left-to-right and then top-to-bottom order, or top-to-bottom then left-to-right order.

You can combine and nest layouts together to build complex user interfaces.

Using Widgets and Layouts: A Practical Example

As an example of how to use widgets and layouts in Kivy, let's look at a commonly used layout class: the GridLayout. With this class, we can create a grid of rows and columns. Each cell of the grid has a unique pair of zero-based coordinates. Consider the following example:

python
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout

ROWS = COLS = 3

class GridApp(App):
    def build(self):
        root = GridLayout(rows=ROWS, cols=COLS)
        for i in range(ROWS):
            for j in range(COLS):
                root.add_widget(Button(text=f"({i}, {j})"))
        return root

GridApp().run()

In the build() method, we instantiate the GridLayout with three rows and three columns. Then use a for loop to add button widgets to the layout using the add_widget() method.

When we run this app, we get the window that is shown below:

Grid Layout in Kivy Grid Layout in Kivy

Each button on the grid shows its corresponding pair of coordinates. The first coordinate represents the row, while the second represents the column. Like the rest of the layout classes, GridLayout can take several arguments that you can use to fine-tune its behavior.

Drawing Shapes in Kivy: The canvas Property

To deeply customize a GUI or design a 2D video game, we may need to draw 2D shapes, such as a rectangle, circle, ellipse, or triangle. Doing this is straightforward in Kivy. The library provides a rich set of shape classes that you can find in the kivy.graphics package. Some of these classes include:

To draw a shape on the screen with Kivy, we need to use the canvas property of a Widget object. This property holds an instance of the Canvas class, which lives in the kivy.graphics package.

Let's see how this works with an example of a white square drawn on the screen:

python
from kivy.app import App
from kivy.core.window import Window
from kivy.graphics import Rectangle
from kivy.uix.widget import Widget

class CanvasApp(App):
    def build(self):
        root = Widget()
        size = 200
        width, height = Window.size
        pos_x = 1/2 * (width - size)
        pos_y = 1/2 * (height - size)
        with root.canvas:
            Rectangle(size=[size, size], pos=[pos_x, pos_y])
        return root

CanvasApp().run()

Inside build(), we create the root widget and define the size of our shape. It'll be a square shape, so each side is equal.

Next, we compute the coordinates to center our shape on the window. The coordinates passed when creating the shape are for the top left corner of the window.

To calculate the correct values, we take the width and height of our main window, halving these values to get the center. We then subtract half of the width or height of our shape to position the center of our shape in the middle of the window. This can be simplified to 1/2 * (width - size) or 1/2 * (height - size). We store the resulting top left coordinates in pos_x and pos_y.

Next, we use the canvas property of our root window to draw the shape. This property supports the with statement, which provides the appropriate context for creating our shapes. Inside the with block, we define our Rectangle instance with the size and pos arguments.

Finally, we return the root widget as expected. The final line of code creates the app instance and calls its run() method. If you run this app from your command line, then you'll get the following window on the screen:

Drawing Shapes in Kivy With Canvas Drawing Shapes in Kivy With Canvas

Cool! You've drawn a square on your Kivy app. The computed coordinates place the square in the center of the window. The default color is white. However, we can change it:

python
# ...
from kivy.graphics import Color, Rectangle
from kivy.uix.widget import Widget
# ...

class CanvasApp(App):
    def build(self):
        # ...
        with root.canvas:
            Color(1, 1, 0, 1)
            Rectangle(size=[side, side], pos=[pos_x, pos_y])
 # ...

In this code snippet, we have added an import for the Color class from the graphics package. The Color class accepts four numeric arguments between 0 and 1 representing the red, green, blue, and transparency components of our target color.

For example, the values (1, 0, 0, 1) represent an entirely red and fully opaque color. The value (0, 1, 0, 0.5) is fully green, half opaque, and half transparent. Consequently, the value (1, 1, 0, 1) gives a fully opaque yellow color. So, if you run the app, then you'll get the following output:

Drawing Shapes in Color With Kivy Drawing Shapes in Color With Kivy

We can experiment with different color values and also with different shape classes, which is cool.

Finally, note that to see the effect of the Color() on the drawn rectangle, the Color class must be instantiated before the Rectangle class. You can think of this as dipping your paintbrush on a palette before using it to paint on your canvas! Interestingly, any drawing that comes after the Color instance is painted accordingly so long as a different color has not been applied.

Using the with statement is pretty convenient and facilitates working with shapes. Alternatively, we can use the canvas.add() method:

python
root.canvas.add(Color(1, 1, 0, 1))
root.canvas.add(
    Rectangle(size=[side, side], pos=[pos_x, pos_y])
)

These statements are equivalent to the statements we have in the with block. Go ahead and give it a try yourself.

Styling Your GUIs With the Kivy Language

Kivy also provides a declarative language known as the Kv language, which aims at separating your application's GUI design and business logic. In this tutorial, we will not go deep into using the Kv language. However, we will highlight some of its main features and strengths.

With Kv language, you can declare and style the widgets and graphical components of your GUI apps. You will put your Kv code in files with the .kv extension. Then you can load the content of these files into your app to build the GUI. You'll have at least two ways to load the content of a .kv file:

  • Relying on the automatic loading mechanism
  • Using the Builder class for manual loading

In the following sections, you'll learn the basics of these two ways of using the Kv language to build the GUI of your Kivy apps.

Relying on the Automatic Widget Loading

As stated earlier, the Kv language helps you separate business logic from GUI design. Let's illustrate this possibility with an updated version of our "Hello, World!" app:

python
from kivy.app import App
from kivy.uix.label import Label

class CustomLabel(Label):
    pass

class MainApp(App):
    def build(self):
        root = CustomLabel()
        return root

MainApp().run()

As you can see we have subclassed the Label class to create a new CustomLabel haven't made any modifications to the subclass, so it functions exactly like the Label class but with a different name. We add a pass statement, which is a Python placeholder statement which makes the code syntactically valid.

Next, create a file called main.kv alongside your app's file. Define a label using the following code:

kv
<CustomLabel>:
    text: "Hello, World!"

Note that your label must have the same name as your custom Python class in the app's file. Additionally, the .kv file must have the same name as your subclass of App, but without the App suffix and in lowercase. In this example, your subclass is named MainApp, so your .kv file must be main.kv.

Now you can run the app from your command line. You'll get the following window on your screen:

Kivy Application Using the Kv Language Kivy Application Using the Kv Language

The Kv language, also known as kivy language or just kvlang, allows us to create widget trees in a declarative way. It also lets you bind widget properties to each other or to callbacks.

Loading Widgets Through the Builder Class

When your Kivy project grows, your .kv file will grow as well. So, it is recommended that you split up the file into different files for readability. In such cases, you will end up with multiple .kv files, and the automatic loading mechanism will not be sufficient. You'll have to use the Builder class from kivy.lang.Builder.

To explore how to use Builder, let's build a sample GUI consisting of a label and button in a BoxLayout. The label will be provided in the labels.kv file, while the buttons will live in the buttons.kv file.

Here's the Python code for this app:

python
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label

Builder.load_file("labels.kv")
Builder.load_file("buttons.kv")

class CustomLabel(Label):
    pass

class CustomButton(Button):
    pass

class MainApp(App):
    def build(self):
        root = BoxLayout(orientation="vertical")
        root.add_widget(CustomLabel())
        root.add_widget(CustomButton())
        return root

MainApp().run()

After importing the required classes, we call the load_file() method. This method takes the filename of a .kv file as an argument and loads it into your app.

Next, you create the custom label and button following the pattern used in the previous section. Inside build(), you create a BoxLayout and add the two widgets to it. Now you need to provide the required .kv files.

Go ahead and create a labels.kv file with the following content:

kv
<CustomLabel>:
    text: "This is a custom label!"
    font_size: 50
    bold: True

This file provides a label with the text "This is a custom label!". Its font will have a size of 50 pixels and will be bold.

The buttons.kv will have the following code:

kv
<CustomButton>:
    text: "Click me!"

Your custom button will be quite minimal. It'll only have the text "Click me!" on it. Go ahead and run the app from your command line. You'll get the following window on your screen:

Kivy Application Using the Kv Language With Multiple kv Files Kivy Application Using the Kv Language With Multiple kv Files

In addition to using the load_file() to build Kv language files, you can also parse and load Kv language directly in a multi-line string in your Python file:

python
Builder.load_string("""
<CustomLabel>:
    text: "This is a custom label!"
    font_size: 50
    bold: True
""")
Builder.load_string("""
<CustomButton>:
    text: "Click me!"
""")

These calls to load_string() are completely equivalent to the corresponding calls to load_file() in our original code example.

Let's take a look at a final example of using the Kv language. This time we'll use the language to draw shapes. Create a rectangle.py file with the following content:

python
from kivy.app import App
from kivy.uix.widget import Widget

class CustomRectangle(Widget):
    pass

class MainApp(App):
    def build(self):
        return CustomRectangle()

MainApp().run()

Now go ahead and create another file in the same directory and save it as main.kv. Then add the following content:

kv
<CustomRectangle>:
  canvas:
    Color:
      rgba: 1, 1, 0, 1
    Rectangle:
      size: 200, 200
      pos: 0, 0

If you run the rectangle.py file, then you will see a 200×200 pixels rectangle ---square in this case--- at the lower left corner of your window! For more guidelines on using Kv Language, check out its official documentation.

More Resources

For some more examples of what you can do with Kivy, take a look at the Kivy examples section in the documentation. Depending on your interest, you can also the other resources. For example:

  • If you're interested in 3D, then the Kivy 3D demo gives a good demonstration of the framework's rendering abilities.
  • If you're interested in using Kivy to develop for mobile, you can write functional Android apps (APKs) with Python and pack them using tools like Buildozer and Python-For-Android without learning Java.
  • If you want a complete vision of where you can use Kivy, then check out the gallery of examples provided by the Kivy community.

Conclusion

What you've learned in this tutorial is just the tip of the Kivy iceberg. There's so much more to Kivy than what meets the eye. It's a powerful GUI library that provides a well-structured hierarchy of classes and objects that you can use to create modern and cross-platform GUIs for your applications.

Top Tips for Great Figma Exports

Prologue

When Qt Design Studio was in its infancy we recognized the need for Designers to be able to import their work into Qt Design Studio (QtDS for the rest of this piece) from other tools. This was before Figma existed and the whole concept of design systems with atomic components was not so well known. At the time we targeted Photoshop, as it had a large user base in the industry and was considered to be an industry standard for UI and UX design. At first our idea was that the Bridge, as we called it, would be mostly useful as an asset importer, a way to get the graphical assets across from one tool to the other, retaining the position and composition, so the Designers would be able to skip the tedious work of exporting and rebuilding the screen design piece by piece.

It quickly became apparent that this would not be enough. Designers wanted to model the application structure, create components and use them as instances, they wanted native qml components for things like rectangles, arcs and well, everything they could. They wanted Variants, Libraries, Anchors, Layouts and Controls. In short there was a lot missing and no tool that could really support it, until of course, Figma came along.

Figma changed the game for a whole lot of Designers, it started to make the design process much closer to how the development process worked. For us it created opportunity to capture the design structure and convert it into qml, the language used in Qt Design Studio, which is an ideal language to describe these kind of design systems.

Each version of Qt Bridge for Figma since then has seen more features added, starting with the simple step of converting a rectangle in Figma into a qml rectangle, then turning variants into states and constraints into anchors, supporting responsive design, library support to enable design systems for teams and most recently converting Figma’s Auto Layouts into the Qt Quick Layouts used in QtDS.

With each new feature added there was of course more complexity, and, as it goes, more for the Designer to do, with more control over the outcome of the design. But all this complexity comes at a price, it means that the Designer has to understand more about the development process and it changes the way you have to design. Structure, nomenclature, interaction and componentization become paramount, the design has to follow rules and the Designer no longer can only concern themselves with how things look.

In order to help new users get up to speed we have created an introduction project for on-boarding and explaining the key concepts of Qt Bridge for Figma. It's what I am basing the examples on in my tips and you can check out that project by trying the Qt Bridge for Figma plugin here:

https://www.figma.com/community/plugin/1167809465162924409

Tip1 - Structure is Everything

Qt Bridge will covert your entire design into qml, every group, every layer, every component and every instance. It will keep all your naming conventions as you have set them, with the caveat that it will resolve duplicates and sanitize the names to conform with qml rules.

So, for your own sanity, review the structure of your design. Remove unwanted layers, don’t nest things in groups inside groups inside groups for no reason and rename all those layer2-copy2 layers to something meaningful!

 

The Layer naming techniques page gives you guidance about how to organize your Figma design for Qt Design Studio.

 

Tip 2 - Use Components, Variants and Instances

Components are the core of your design system and really, everything in your design should be a component or an instance of one. Don't be afraid to use components inside components, building up nested components from the atomic level into more complex widgets will make it easier to keep control of your design system. 

Figma variants inside a component will be converted into qml states when you import them into QtDS. This means you should be using the variants for all the different visual states of your design, you can already set the state of these components in Figma and QtDS will show you the correct state in your exported design.

 

Variants get converted into qml states inside your component.

 

Tip 3 - Use Constraints to make your Components scalable

Constraints are crucial to creating responsive design, allowing components to be used at different scales. Adding the correct constraints to your components means that they will always work as intended in your design, Qt Bridge for Figma can automatically convert your constraints into qml anchors. 

Constraints converted to Anchors create responsive design.

 

Tip 4 - Use Auto Layouts

Layouts are similar to Constraints, in that they will make your content responsive, the difference is that with a layout you can add or remove items to it dynamically and the layout will take care of arranging all the content within it. Qt Bridge will automatically convert your Figma Auto Layouts into Qt Quick Layouts, allowing you to build complex dynamic UI's in Qt Design Studio. 

Auto Layouts converted to Qt Quick Layouts react to added or removed content.

 

Tip 5 - Use Libraries to publish your Components

This tip is only relevant for Designers using a paid tier but if you happen to be on a professional or enterprise plan, you really need to be using Libraries. Publishing a set of tested components means you can maintain a single source of truth for your design and trust that it will always export properly to QtDS. Figma’s publishing tools make it simple to manage what is part of a library and what is not, making it a breeze to manage large and complex projects with a vast amount of components across multiple teams and projects. Qt Bridge handles all the dependencies for you so you only export the components you need.

Figma Libraries allow you to decide what gets published, Qt Bridge can resolve all the dependencies.

 

Tip 6 - Use Sections to exclude design from your export

This is a quick tip but a useful one, even with the best organization there is usually some part of the design project you don’t want in the UI code itself, maybe some sketches or an experimental design that you want to come back to later. Designers can select these on their canvas and put it in a section, which Qt Bridge will automatically skip on export.

Sections are an easy way to exclude content from the export.

 

Tip 7 - Use the right Objects in Figma to get the right Asset in Qt

Not everything in your Figma file will get turned into an Asset such as a png or svg, in fact many of the "Types of Things" you can create in Figma will get translated into a native qml Type on import. So for example, a Rectangle in Figma becomes a Qt Quick Rectangle in Qt Design Studio, ellipses and arcs become their equivalent Types in qml. Vectors become SVG paths and of course Text Items become Text Items. This means you can change, create states, animate or link to another value with a binding, the various properties of this item in QtDS. Properties such as color, or font, or arc end position are not static, as they would be in a generated image asset.

If we can't find an appropriate native item to convert into, then we will fall back, as gracefully as possible, to assets.

Native items have properties that can be changed further in Qt Design Studio.

 

Tip 8 - Use Control Templates to create Controls

Using our Control Templates brings together the best of Figma and Qt Quick Controls. Allowing you to design and re-design your control sets in Figma directly and still have the powerful features of Qt Quick Controls, without having to code anything. You can learn more about control templates in my previous blog post here:

https://www.qt.io/blog/qt-quick-control-templates-for-figma

Control Templates generate working Quick Controls directly from Figma.

 

Tip 9 - Go check out the Project!

There’s more information and guidance there to read. Export it and look at the results in Qt Design Studio.

You can get the project by trying out the plugin here:

https://www.figma.com/community/plugin/1167809465162924409

 

 

Value Semantics

C++ is an old language. Many aspects of our programming styles have become habits that we do not think about too much today. In this blog, I’m going to address one major issue that has resulted from such habits and get you prepared for a new bright world of C++2b. So without further ado, let’s dive!

Identifying a Problem

There is a problem that we don’t want to face. It’s called reference semantics. Reference semantics tells us that the variable is referenced by one or more places in code, like a crossroads, leading to a center of the city. Let’s address an elephant in the room: the global variable.

You probably already know that the global variable is a big no-no in the C++ community because the prediction of the state of a global variable is borderline impossible, if it’s writable and readable. If the code is concurrent, it becomes a big pain to synchronize all the ins and outs to make it work.

Thus, global variables are frowned upon. Though we’ve become pretty good at replacing them with other things, the issue still exists within references. The global variable is, in its core, a reference to memory that is globally allocated. It has a way to be addressed from any point in code, adding roads to the crossroads of reference semantics. It’s the same as a reference or a pointer to something, just on a larger scale.

So, what’s the cost of it? Upon expansion, programs become more coupled and fragile, accumulating technical debt. Removing a global variable from a program is hard, but the same goes for anything that is shared or pointed to in a non-unique manner.

We are not going to tackle all the problems the reference semantics cause today. Instead, we’ll move on to and focus on function calls.

Call Safe, Not Fast.

Why focus on functions? Because we need to specify the guarantees that we, as programmers, give to our library users.

First, let’s define some common value semantics rules for easier understanding.

Those rules are:

  1. Regularity: the state of an object is equivalent to its copy.
  2. Independence: the state of an object remains constant for the local scope and can be changed only by a local scope.

Those give us the following conditions:

  • Values can’t be written by operations on other values.
  • Writing the value does not affect other values.

Let’s look at an example:

void add(int& a, const int&b){
    a+=b;
}
void add2(int& a, const int&b){
   add(a,b);
   add(a,b);
}

int main(){
    int x = 10;
    add2(x,x);
    return x;
}

Even if it is somewhat artificial, can you guess what the output will be? If you said 40, congratulations!

But why is it 40? Add adds const& b to a. So the first time, it should be 20 and, the second time, it’s 30! Well, we passed x as a const reference, even though its const didn’t give any guarantee that the value is not going to change by external factors. So const& is not const enough.

Even if we designed and documented the usage of the function, the model of human thinking ignores the possibility of overlapping values. Additionally, the documentation often does not help with that, since we assume the library user knows that already.

The solution would be to pass the ints by value and return them by value, thus fulfilling the value semantics rules.

But what about for larger types? For those, the possible solutions would be to wrap them in the unique pointer and return it. Move semantics help us in that regard. The shared pointer is another way to ensure lifetime, but because C++ does not have borrowed checker there may be some implications with concurrent usage of shared pointers.

Accelerating!

We’ve looked at the problems with data coherency. Now, let’s talk about lifetimes. Assume we have a function:

auto f(const std::vector<int>& vec, class SomeClass& to_fill);

We know that data isn’t overlapping and “to_fill” is not modifying the vec in any circumstance. But still, there is a phantom menace lurking around. Let’s define some words about function execution:
the function gives strong guarantee of execution if the selected scenario of execution provides strong lifetime to its arguments and the internal state of the function. Basically, it’s a way to say that on current execution will not result in UB under any conditions.

int main()
{
    SomeClass c;
    std::array<int, 4> arr{1,2,3,4};
    f(arr, c);
}

This call is strong because the lifetimes of the objects extend beyond function call.

But now, what if the function “f” is asynchronous, either a regular async function or an eager coroutine?

Task f(const std::vector<int>& vec, class SomeClass& to_fill)
{
        co_await resume_on_threadpool();
        // Do some expensive calculations
}

Now this function returns immediately after resume_on_threadpool() is called. Suddenly, all the objects are destroyed, even before the execution is finished. Now, references and reference semantics become a big evil. Because we can’t guarantee the lifetimes of objects, function becomes weak and, if not executed within synchronous context or not “co_await”ed upon, may and probably will result in UB.

This is where value semantics come to the rescue at last! We can change vector& to vector itself and move in the value.

But what should we do with the class here? If we assume it comes from another part of a program that has it instantiated, the best bet is to use smart pointers.

Task<std::unique_ptr<SomeClass>> f(
std::vector<int> vec, 
std::unique_ptr<SomeClass>> to_fill
);

Now the execution is ensured to run safely.

Conclusion

Value semantics is a great tool for ensuring the stability of your code. Use it sparingly, because C++ is eagerly copying everything you pass into functions, sometimes resulting in performance loss.

Because asynchronous programming is coming towards us at an astonishing speed with coroutines, we need to consider the correct behavior and the lifetime of arguments for more use cases.

Thank you for reading. There is much more I could share, but this would quickly become an unusually long blog if I were to share it all here. Stay tuned for more advanced C++ in the future.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Value Semantics appeared first on KDAB.

KDDockWidgets 1.7.0 Released

We are happy to announce the release of KDDockWidgets version 1.7.0!

What is KDDockWidgets?

KDDockWidgets is a development framework for custom-tailored docking systems in Qt, to use when you need advanced docking that is not supported by QDockWidgets. It was created by Sérgio Martins as a time-saving alternative to QDockWidgets. The ease-of-use of KDDockWidgets can save you lots of frustration as well, in that you won’t have to deal with the myriad bugs and the difficulties and complexities faced when working with QDockWidgets.

What’s new in release 1.7.0?

KDDockWidgets is continuously maturing, with more features available for its users.

A few of the new additions found in this update are among others:

We introduced DockWidget::setFloatingWindowFlags(flags), which allows for different FloatingWindows to have different window flags. As an example, some have Qt::Tool while others have Qt::Window. Complementary to that it is now possible to specify Qt::Tool or Qt::Window per floating window. Furthermore, you are able to restore layouts without touching floating dock widgets.

If you are looking for a development framework for custom-tailored docking systems, KDDockWidgets should definitely be on your list. The latest version, 1.7.0, offers more options and an even more seamless experience for developers looking for advanced docking.

For more information

To find out more about this release’s highlights and how to download it, click here.

To read more about KDDockWidgets, visit this page.

 

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post KDDockWidgets 1.7.0 Released appeared first on KDAB.

PyQt6 Book: 파이썬과 Qt6로 GUI 애플리케이션 만들기

I am very happy to announce that my Python GUI programming book Create GUI Applications with Python & Qt6 / PyQt6 Edition is now available in Korean from Acorn Publishing

It's more than a little mind-blowing to see a book I've written translated into another language -- not least one I cannot remotely understand! When I started writing this book a few years ago I could never have imagined it would end up on book shelves in Korea, never mind in Korean. This is just fantastic.

파이썬과 Qt6로 GUI 애플리케이션 만들기 파이썬과 Qt6로 GUI 애플리케이션 만들기 파이썬 애플리케이션 제작 실습 가이드

If you're in Korea, you can also pick up a copy at any of the following bookstores: Kyobobook, YES24 or Aladin

Thanks again to Acorn Publishing for translating my book and making it available to readers in Korea.

For more, see the complete PyQt6 tutorial.

Shader Variants

Background of Shaders

One particular facet of modern graphics development that is often a pain – even for AAA games — is shader variants!

If you have bought an AAA game in recent years and wondered what the heck it is doing when it says it is compiling shaders for a long time (up to an hour or more for some recent PC titles on slower machines!), then this blog will explain it a little.

Modern graphics APIs (Vulkan, D3D12, Metal) like to know about everything that has to do with GPU state, up front. A large chunk of the GPU state is provided by so-called shader programs. These shader programs fill in various gaps in the graphics pipeline that used to be provided by fixed-function hardware back in the days of OpenGL 1.x.

As OpenGL (and DirectX) evolved, people wanted to do a wider range of things when processing vertices into colorful pixels on-screen. So, over time, the fixed function silicon on GPUs has gradually been replaced by more and more general purpose processors. As with CPUs, we now need to tell these processors what to do by writing small (sometimes larger), specialized programs called shader programs.

In OpenGL, we would write our shaders in the high-level GLSL language and feed that to the OpenGL driver as a string at runtime. The OpenGL driver would then compile the GLSL to GPU machine code and we could then throw big piles of vertices and other resources like textures at it and marvel at the results — or, more likely, swear a bit and wonder why we are staring at a black window yet again.

The necessity of including a complete compiler in the graphics driver was a huge burden for each of the GPU vendors, resulting in a great deal of overhead for them. It also led to some strange problems for developers when running code on a new platform with a different GLSL compiler in the driver and hitting new and different bugs or shortcomings.

With the advent of modern graphics APIs, there has been a move toward consuming shader code in the form of a bytecode intermediate representation, such as SPIR-V. SPIR-V is still not the final form of executable code required by the GPU silicon but it is much closer to it than GLSL and means the Vulkan drivers no longer need the entire compiler front-end.

Tooling, such as nSight and RenderDoc, are able to decompile the SPIR-V shader code back to GLSL (or HLSL) to make it easier for you to debug your applications.

The conversion from GLSL (or any other suitable language) to SPIR-V can still happen at runtime if that’s what you need — for example, in dynamic editor tools. However, for constrained applications, we can now compile the GLSL to SPIR-V up front at build time.

That’s nice! We can simply add a few targets to our CMakeLists.txt and go home, right? Well, not quite.

The Need for Shader Variants

You see, shader developers are just as lazy as any other kinds of developers and like to reduce the amount of copy/paste coding that we have to do. So, we add optional features to our shaders that can be compiled in or out by way of pre-processor #defines, just as with C/C++.

Why is this even needed, though? Well, we don’t always have full control over the data that our application will be fed. Imagine a generic glTF file viewer application. Some models that get loaded will use textures for the materials and include texture coordinates in the model’s vertex data. Other models may just use vertex colors, completely leaving out texture coordinates.

To handle this, our vertex shader’s prologue may look something like this:


layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec3 vertexNormal;
#ifdef TEXCOORD_0_ENABLED
layout(location = 2) in vec2 vertexTexCoord;
#endif
 
layout(location = 0) out vec3 normal;
#ifdef TEXCOORD_0_ENABLED
layout(location = 1) out vec2 texCoord;
#endif

Then, in the main() function, we would have:


void main()
{
#ifdef TEXCOORD_0_ENABLED
    texCoord = vertexTexCoord;
#endif
    normal = normalize((camera.view * entity.model[gl_InstanceIndex] * vec4(vertexNormal, 0.0)).xyz);
    gl_Position = camera.projection * camera.view * entity.model[gl_InstanceIndex] * vec4(vertexPosition, 1.0);
}

The fragment shader would have similar changes to handle the cases with and without texture coordinates.

Super, so we have one set of shader source files that can handle both models with textures and models without textures. How do we compile the shaders to get these shader variants?

Just as with C/C++ we have a compiler toolchain and, similarly, we invoke the compiler with the various -D options as needed, e.g.:


glslangValidator -o material-with-uvs.vert.spirv -DTEXCOORD_0_ENABLED material.vert    # With texture coords
glslangValidator -o material-without-uvs.vert.spirv material.vert                      # Without texture coords

Then, within our application, we can load the glTF model, inspect its data to see whether it uses textures, and then load the appropriate SPIR-V compiled shader.

Hooray! The job is done and we can go home now, right? Well, actually, no — the project manager just called to say we also need to handle models that include the alpha cut-off feature and models that don’t include it.

Alpha cut-off is a feature of glTF files by which any pixels determined to have an alpha value less than some specified threshold simply get discarded. This is often used to cut away the transparent parts of quads used to render leaves of plants.

Ok then — let’s simply repeat a process similar to that which we did for handling the presence, or absence, of texture coordinates.

The fragment shader implementation of alpha cut-off is trivial:


void main()
{
    vec4 baseColor = ...;
#ifdef ALPHA_CUTOFF_ENABLED
    if (baseColor.a < material.alphaCutoff)
        discard;
#endif
    ...
    fragColor = baseColor;
}

We can then add suitable CMake targets to compile with and without this option.

Of course, there’s a catch. We have a combinatorial explosion of feature combinations. This only gets worse when we add the next optional feature or optional features that have various settings we wish to set at compile time, such as the number of taps used when sampling from a texture to perform a Gaussian blur.

Clearly, we do not want to have to add several thousand combinations of features as CMake targets by hand! So, what can we do?

Exploring the Problem

Let’s consider the above combination of the texture coordinates and alpha cut-off features. Our table of features and compiler flags looks like this:

Tex Coord Off Tex Coord On
Alpha Cut-off Off -DTEXCOORD_0_ENABLED
Alpha Cut-off On -DALPHA_CUTOFF_ENABLED -DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED

Adding another option would add another dimension to this table. The above mentioned option of blur filter taps with, say, 3, 5, 7, or 9 taps would add a 3rd dimension to the table and increase the number of options by another factor of 4, for a total of 16 possible configurations of this one shader program.

Adding just a handful of features, we can see that it would be all too easy to end up with thousands of combinations of compiled shaders from the single set of GLSL files!

How can we solve this in a nice and extensible way?

It is easy enough to have nested loops to iterate over the available options for each of the specified axes of variations. But what if we don’t know all of the axes of variation up front? What if they vary from shader to shader? Not all shaders will care about alpha cut-off or blur filter taps, for example.

We can’t simply hard-wire a set number of nested loops to iterate over the combinations in our CMake files. We need something a bit more flexible and smarter.

Let’s think about the problem in a slightly different way.

To start with, let’s represent a given configuration of our option space by a vector of length N, where N is the number of options. For now, let’s set this to 3, for our options we have discussed:

  1. Texture Coordinates (Off or On)
  2. Alpha Cut-off (Off or On)
  3. Blur filter taps (3, 5, 7, or 9)

That is, we will have a vector like this:

[TexCoords Off, Alpha Cut-off Off, blur taps = 3]

To save some typing, let’s now replace the wordy description of each element with a number representing the index of the option for that axis of variation:

  1. Texture Coordinates: (0 = Off, 1 = On)
  2. Alpha Cut-off: (0 = Off, 1 = On)
  3. Blur filter taps: (0 = 3 taps, 1 = 5 taps, 2 = 7 taps, 3 = 9 taps)

With this scheme in place, our above option set will be:

[0, 0, 0]

And the vector representing texture coordinates on, no alpha cut-off, and 7 blur filter taps option will be:

[1, 0, 2]

How does this help us? Well, it allows us to succinctly represent any combination of options; but it’s even better than that. We can now easily go through the list of all possible combinations in a logical order. We begin by incrementing the final element of the vector over all possible values. Then we increment the previous element and repeat, like this:

[0, 0, 0]
[0, 0, 1]
[0, 0, 2]
[0, 0, 3]
[0, 1, 0]
[0, 1, 1]
[0, 1, 2]
[0, 1, 3]
[1, 0, 0]
[1, 0, 1]
[1, 0, 2]
[1, 0, 3]
[1, 1, 0]
[1, 1, 1]
[1, 1, 2]
[1, 1, 3]

Note that the total number of option combinations is just the product of the number of options in each dimension or axis of variation, e.g. 2x2x4 = 16 in this example.

The above sequence is exactly what we would get if we had 3 nested for-loops to iterate over the options at each level. How does this help us?

Well, looking at the above sequence of options vectors, you may well notice the similarity to plain old counting of numbers. For each “decimal place” (element in the vector), starting with the final or least significant digit, we go up through each of the available values. Then, we increment the next least significant digit and repeat.

The only difference to how we are used to counting in decimal (base 10), binary, octal, or hexadecimal is that the base of each digit is potentially different. The base for each digit is simply the number of options available for that axis of variation (e.g. the texture coordinates can only be on or off (base = 2)). It’s the same for the alpha cut-off. The blur taps option has a base of 4 (4 possible options).

We know how many combinations we need in total and we know that each combination can be represented by a vector that acts like a variable-base number. Therefore, if we can find a way to convert from a decimal number to the corresponding combination vector, we are in a good situation, as we will have converted a recursive approach (nested for-loops) into a flat linear approach. All we would need would be something like this pseudo-code:


for i = 0 to combination_count
   option_vector = calculate_option_vector(i)
   output_compiler_options(option_vector)
next i

So how do we do this?

A Solution

To convert a decimal number into a different base system is fairly easy. The process is described well at https://www.tutorialspoint.com/computer_logical_organization/number_system_conversion.htm, where they give an example of converting from decimal to binary.

All we have to do, in our case, is use a base that differs for each digit of our combination vector. However, before we show this, we need a way to specify the options for each shader that we wish to consider. We have done this by way of a simple JSON file, for now. Here is an example showing our above case for these options as applied to the fragment shader, but only the texture coordinates and alpha cut-off for the vertex shader. This is just an example for illustration. In reality, the vertex shader has nothing to do with alpha cut-off and our simple shaders do not do anything with the blur tap option at all:


{
    "options": [
        {
            "name": "hasTexCoords",
            "define": "TEXCOORD_0_ENABLED"
        },
        {
            "name": "enableAlphaCutoff",
            "define": "ALPHA_CUTOFF_ENABLED"
        },
        {
            "name": "taps",
            "define": "BLUR_TAPS",
            "values": [3, 5, 7, 9]
        }
    ],
    "shaders": [
        {
            "filename": "materials.vert",
            "options": [0, 1]
        },
        {
            "filename": "materials.frag",
            "options": [0, 1, 2]
        }
    ]
}

The default in our system, if no explicit options are provided in the JSON file, is defined (on) or not defined (off).

Each input shader file section then specifies which of the options it cares about. So, in this example, the fragment shader considers all 3 options and will have 16 variants compiled.

In order to generate the possible build combinations, we have written a small Ruby script to implement the necessary logic. Why Ruby? Because I couldn’t face trying to do the necessary math in CMake’s scripting language and Ruby is lovely!

The core of the script that implements the decimal to a variable-base number (combination vector) is pretty simple:


def calculate_digits(bases, index)
  digits = Array.new(bases.size, 0)
  base_index = digits.size - 1
  current_value = index
  while current_value != 0
    quotient, remainder = current_value.divmod(bases[base_index])
    digits[base_index] = remainder
    current_value = quotient
    base_index -= 1
  end
  return digits
end

In the above code, the bases argument is a vector representing the base of each digit in the final combination vector. Here, bases = [2, 2, 4]. We then loop over the decimal number, performing the divmod operation at each step to find the value of each digit in our combination vector. When we have reduced the input decimal number to 0, we are done. This is exactly analogous to the decimal to binary conversion linked above but for variable base at each digit.

With the resulting combination vector in hand, it is simple for us to then look up the corresponding compiler -D option for that selection and output that into a JSON string. Here is an example of the output of running the ruby script against the above configuration file:


{
  "variants": [
    {
      "input": "materials.vert",
      "defines": "",
      "output": "materials.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DALPHA_CUTOFF_ENABLED",
      "output": "materials_alpha_cutoff_enabled.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DTEXCOORD_0_ENABLED",
      "output": "materials_texcoord_0_enabled.vert.spv"
    },
    {
      "input": "materials.vert",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled.vert.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=3",
      "output": "materials_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=5",
      "output": "materials_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=7",
      "output": "materials_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DBLUR_TAPS=9",
      "output": "materials_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=3",
      "output": "materials_alpha_cutoff_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=5",
      "output": "materials_alpha_cutoff_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=7",
      "output": "materials_alpha_cutoff_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=9",
      "output": "materials_alpha_cutoff_enabled_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=3",
      "output": "materials_texcoord_0_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=5",
      "output": "materials_texcoord_0_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=7",
      "output": "materials_texcoord_0_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DBLUR_TAPS=9",
      "output": "materials_texcoord_0_enabled_blur_taps_9.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=3",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_3.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=5",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_5.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=7",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_7.frag.spv"
    },
    {
      "input": "materials.frag",
      "defines": "-DTEXCOORD_0_ENABLED -DALPHA_CUTOFF_ENABLED -DBLUR_TAPS=9",
      "output": "materials_texcoord_0_enabled_alpha_cutoff_enabled_blur_taps_9.frag.spv"
    }
  ]
}

If you are interested, this is the full script:


require 'json'
require 'pp'

def expand_options(data)
  # Expand the options so that if no explicit options are specified we default
  # to options where the #define symbole is defined or not
  data[:options].each do |option|
    if !option.has_key?(:values)
      option[:values] = [:nil, :defined]
    end
    option[:count] = option[:values].size
  end
end

def extract_options(data, shader)
  shader_options = Hash.new
  shader_options[:options] = Array.new
  shader[:options].each do |option_index|
    shader_options[:options].push data[:options][option_index]
  end
  # STDERR.puts "Options for shader:"
  # STDERR.puts shader_options
  return shader_options
end

def find_bases(data)
  bases = Array.new(data[:options].size)
  (0..(data[:options].size - 1)).each do |index|
    bases[index] = data[:options][index][:count]
  end
  return bases
end

def calculate_steps(bases)
  step_count = bases[0]
  (1..(bases.size - 1)).each do |index|
    step_count *= bases[index]
  end
  return step_count
end

# Calculate the number for "index" in our variable-bases counting system
def calculate_digits(bases, index)
  digits = Array.new(bases.size, 0)
  base_index = digits.size - 1
  current_value = index
  while current_value != 0
    quotient, remainder = current_value.divmod(bases[base_index])
    digits[base_index] = remainder
    current_value = quotient
    base_index -= 1
  end
  return digits
end

def build_options_string(data, selected_options)
  str = ""
  selected_options.each_with_index do |selected_option, index|
    # Don't add anything if option is disabled
    next if selected_option == :nil

    # If we have the special :defined option, then we add a -D option
    if selected_option == :defined
      str += " -D#{data[:options][index][:define]}"
    else
      str += " -D#{data[:options][index][:define]}=#{selected_option}"
    end
  end
  return str.strip
end

def build_filename(shader, data, selected_options)
  str = File.basename(shader[:filename], File.extname(shader[:filename]))
  selected_options.each_with_index do |selected_option, index|
    # Don't add anything if option is disabled
    next if selected_option == :nil

    # If we have the special :defined option, then we add a section for that option
    if selected_option == :defined
      str += "_#{data[:options][index][:define].downcase}"
    else
      str += "_#{data[:options][index][:define].downcase}_#{selected_option.to_s}"
    end
  end
  str += File.extname(shader[:filename]) + ".spv"
  return str
end


# Load the configuration data and expand default options
if ARGV.size != 1
  puts "No filename specified."
  puts "  Usage: generate_shader_variants.rb "
  exit(1)
end

variants_filename = ARGV[0]
file = File.read(variants_filename)
data = JSON.parse(file, { symbolize_names: true })
expand_options(data)

# Prepare a hash to output as json at the end
output_data = Hash.new
output_data[:variants] = Array.new

data[:shaders].each do |shader|
  # STDERR.puts "Processing #{shader[:filename]}"

  # Copy over the options referenced by this shader to a local hash that we can operate on
  shader_options = extract_options(data, shader)

  # Create a "digits" array we can use for counting. Each element (digit) in the array
  # will correspond to an option in the loaded data configuration. The values each
  # digit can take are those specified in the "values" array for that option.
  #
  # The number of steps we need to take to count from "0" to the maximum value is the
  # product of the number of options for each "digit" (option).
  bases = find_bases(shader_options)
  # STDERR.puts "Bases = #{bases}"
  step_count = calculate_steps(bases)
  # STDERR.puts "There are #{step_count} combinations of options"

  # Count up through out range of options
  (0..(step_count - 1)).each do |index|
    digits = calculate_digits(bases, index)

    selected_options = Array.new(bases.size)
    (0..(bases.size - 1)).each do |digit_index|
      settings = data[:options][digit_index]
      setting_index = digits[digit_index]
      selected_options[digit_index] = settings[:values][setting_index]
    end

    # Construct the options to pass to glslangValidator
    defines = build_options_string(shader_options, selected_options)
    output_filename = build_filename(shader, shader_options, selected_options)

    # STDERR.puts "  Step #{index}: #{digits}, selected_options = #{selected_options}, defines = #{defines}, output_filename = #{output_filename}"

    variant = { input: shader[:filename], defines: defines, output: output_filename }
    output_data[:variants].push variant
  end

  # STDERR.puts ""
end

puts output_data.to_json


Integrating into the Build System

CMake is now able to read and parse JSON documents — a fact that I didn’t know at first. This means that we can quite conveniently ask our build system to execute our Ruby script as an external process at configure time, capture the JSON output as shown above, iterate over the generated combinations, and add a build target for each one.

The cut-down code for doing this is:


function(CompileShaderVariants target variants_filename)
    # Run the helper script to generate json data for all configured shader variants
    execute_process(
        COMMAND ruby ${CMAKE_SOURCE_DIR}/generate_shader_variants.rb ${variants_filename}
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        OUTPUT_VARIABLE SHADER_VARIANTS
        RESULT_VARIABLE SHADER_VARIANT_RESULT
    )
 
    if(NOT SHADER_VARIANT_RESULT EQUAL "0")
        message(NOTICE ${SHADER_VARIANT_RESULT})
        message(FATAL_ERROR "Failed to generate shader variant build targets for " ${variants_filename})
    endif()
 
    string(JSON VARIANT_COUNT LENGTH ${SHADER_VARIANTS} variants)
    message(NOTICE "Generating " ${VARIANT_COUNT} " shader variants from " ${variants_filename})
 
    # Adjust count as loop index goes from 0 to N
    MATH(EXPR VARIANT_COUNT "${VARIANT_COUNT} - 1")
 
    foreach(VARIANT_INDEX RANGE ${VARIANT_COUNT})
        string(JSON CURRENT_INTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} input)
        string(JSON CURRENT_OUTPUT_FILENAME GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} output)
        string(JSON CURRENT_DEFINES GET ${SHADER_VARIANTS} variants ${VARIANT_INDEX} defines)
 
        set(SHADER_TARGET_NAME "${target}_${CURRENT_OUTPUT_FILENAME}")
        CompileShader(${SHADER_TARGET_NAME} ${CURRENT_INTPUT_FILENAME} ${CURRENT_OUTPUT_FILENAME} ${CURRENT_DEFINES})
    endforeach(VARIANT_INDEX RANGE ${VARIANT_COUNT})
endfunction()

Here, CompileShader() call is another helper function that just invokes the glslangValidator GLSL->SPIR-V compiler with the specified options.

This nicely takes care of generating all of the required shader variants that will be compiled with correct dependencies on the source GLSL files. To ensure that the targets get updated if the input JSON configuration file changes, we can add the following snippet to the above function:


# Re-run cmake configure step if the variants file changes
set_property(
    DIRECTORY
    APPEND
    PROPERTY CMAKE_CONFIGURE_DEPENDS ${variants_filename}
)

Now, if we edit the JSON configuration file that contains the options, CMake will automatically re-run and generate the targets.

On the C++ runtime side of things, we have some logic to construct the appropriate shader file name for the compiled SPIR-V shader matching the options needed by whatever model we are rendering.

In the future, we may make this part more reusable by making it read in the same JSON configuration file used to create the shader variants.

Wrapping Up

So, going back to where we started: how does all of this tie into your PC’s spending an hour compiling shaders when we have shown here how to compile them at application build time?

It all goes back to SPIR-V’s just being a bytecode intermediate representation. Before the GPU can execute these shaders, it needs to do a final compilation step to convert the SPIR-V to actual machine code. In a modern graphics API, this is done when we create a so-called “graphics pipeline.” At this point, we have to specify pretty much all GPU state, which then gets baked into a binary blob along with the shader code by the driver. This binary blob is both GPU-vendor and driver-version specific. So, it cannot be built at application build time but, rather, has to be done on the actual machine on which it will execute.

The first time you run such a game or other application, it will often loop through all of the shader variants and compile a graphics pipeline for each one. These then get cached to disk for use on subsequent runs. If you change your GPU or (more likely) the driver version, then this cache might get invalidated and you’d have to sit through this process once again.

For systems with known hardware and drivers, this whole process can be performed as part of the build step. This is why consoles such as the PlayStation 5 do not have to do this lengthy shader compiling step, while we wait there and watch.

There is some work going on in Khronos at present, in the shape of VK_ext_shader_object, to try to get back to a more dynamic-shader friendly way of doing things, in which the driver takes care of much of this compiling and caching for us. As with all things in computer science though, it will be a trade-off.

Thank you for reading about what turned out to be a nice little excursion of simplifying a problem by changing it from recursive to linear and learning about converting between numbers of different bases.

If you would like to learn more about modern 3D graphics or get some help on your own projects, then please get in touch.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Shader Variants appeared first on KDAB.

Validate User Input When Creating Apps With Tkinter and Python

When writing GUI applications you often need to accept data from users. A reliable application must verify and validate all its input data before taking any further action or doing any processing. Input validation is the process of examining the data your users provide to your applications to ensure its validity.

In this tutorial, we'll look at some examples of common input data validation strategies you can use when developing GUI applicatipons with the Tkinter UI library.

Input Validation Strategies in GUI Apps

Input data validation is commonly needed in dialog windows where we provide input widgets for the user to enter information that our program requires to work properly. When validating input data in a dialog or any other component of our app's GUI, there are two main approaches we can take:

  1. Real-time validation: This strategy is also called widget-level validation and involves validating the user's input as soon as they enter it in the app's GUI.
  2. Batch validation: This strategy is also known as form-level validation and involves validating all input fields at once, usually when the user clicks the submit button on a dialog.

Real-time validation works best in situations where input data is continuously entered and needs to be verified quickly. For example, when we're using a table widget to update a table in a database in real time.

Batch validation is suitable when there are interdependencies between different pieces of input. In this case, the input data needs to be validated all at once. A good example of this type of validation is a login form which typically verifies the user and the password at once when we click the Login button.

In the following sections, you'll learn how to implement these two validation strategies in a Tkinter application.

Form-Level Input Validation in Tkinter

Tkinter is a popular GUI library in Python. It provides a basic set of widgets that you can use for building GUI applications quickly. The library comes with a few widgets for data entry, including Entry, Text, Spinbox, and others.

To demonstrate the form-level validation strategy, we'll use the Entry widget, which presents a simple text box to the user. For this example, we'll create a form with an Entry and a Button. The Entry widget will only accept numeric values. We'll run the input data validation when the user clicks on the button to submit the information for processing.

Below is the starter code for our small Tkinter app:

python
from tkinter import Tk, ttk

# Create the app's main window
root = Tk()
root.title("Form-Level Input Validation")
root.geometry("490x100")

# Add widgets
entry = ttk.Entry(root, width=35)
entry.grid(row=0, column=0, padx=5, pady=5)
button = ttk.Button(root, text="Validate", command=validate_numbers)
button.grid(row=0, column=1, padx=5, pady=5)
label = ttk.Label(root, text="Display")
label.grid(row=1, column=0, columnspan=2, padx=5, pady=5)

# Run the app's main loop
root.mainloop()

In the first line of the code, we import Tk and ttk from the tkinter package. Tk is Tkinter's main GUI class which provides the application window and main loop. The ttk module provides styled widgets for you to create modern Tkinter apps.

Next, we create the parent window, root, by instantiating the Tk class. The window will have a title and a geometry (shape), which we set using the title() and geometry() methods.

Then, we create three different widgets: an entry, a label, and a button using the ttk.Entry, ttk.Label and ttk.Button classes, respectively.

To create the Entry widget, we use the parent window, root, and width as arguments. To place the entry widget in the parent window, we use the grid() geometry manager. This lays widgets out in a grid, at the specified row and column positions, with padx and pady (horizontal and vertical padding) in pixels.

Then we create a button using the ttk.Button class with the parent window and text as arguments. Again, we use grid() to place the button on the parent window, on the same row as the Entry but now in column 1.

Finally, we create a ttk.Label widget using a parent window and text as arguments. Using the grid() method, we place this on the next row in column 0, using colspan=2 to stretch it across two columns.

With our application complete, we run it by calling mainloop() on root. You'll get the following output:

An example Tkinter application with Entry, Button, and Button widgets An example Tkinter application with Entry, Button, and Label widgets

Great! We have a working Tkinter app with a simple form.

Let's jump into input validation. So far, the button doesn't do anything when we click it. We want the button to run code for validating the data provided through the entry widget. Let's create a function for validating numeric values:

python
from tkinter import Tk, ttk

# Create the app's main window
root = Tk()
root.title("Form-Level Input Validation")
root.geometry("490x100")

# Create a validation function
def validate_numbers():
    input_data = entry.get()
    if input_data:
        try:
            float(input_data)
            label.config(
                text=f"Valid numeric value: {input_data}",
                foreground="green",
            )
        except ValueError:
            label.config(
                text=f'Numeric value expected, got "{input_data}"',
                foreground="red",
            )
    else:
        label.config(
            text="Entry is empty",
            foreground="red",
        )

# ...

Here we have implemented a validate_numbers() function to test if the user's input is a valid number. Inside the function, we get the input data from the entry widget using the get() method, storing it in the variable input_data.

First, we use an if statement to check whether input_data contains anything at all. If input_data is an empty string, which is falsey, the else clause will change the label's text to "Entry is empty" in red.

If input_data contains some text, we further validate this by attempting to cast the value to a float using the built-in float() type. If the value is not a valid float, the code will throw a ValueError exception. If this occurs, we catch this exception and display an informative message in red.

Finally, in the case where input_data contains something which is a valid numeric value, a success message will be shown in green.

Now that we have implemented our validation function, we can bind it to our button by passing it as the command argument. Update the Button instantiation as follows:

python
# Add widgets
# ...
button = ttk.Button(root, text="Validate", command=validate_numbers)
# ...

Here, the command argument points to the function that we want to trigger when the button is clicked. If you run the program again and enter valid numbers, you'll get an output similar to the following when clicking the Validate button:

Entry box with a valid number, showing the success message Entry box with a valid number, showing the success message

Great, the app validated the input successfully! What will happen if we provide an invalid input? Check out the form below:

Entry box with an invalid number, showing the failure message Entry box with an invalid number, showing the failure message

Great! Everything works! If you enter something which is not a number, your app will now correctly identify the invalid input.

Widget-Level Input Validation in Tkinter

With the widget-level validation strategy, we validate the input data using widget-specific events. For example, an input validation function can run when an entry widget gets or loses focus. This approach comes in handy when we want to validate input data in real time, widget by widget.

To see this strategy in action, let's create a window with two entry widgets. The code for that is as follows:

python
from tkinter import Tk, ttk

# Create the app's main window
root = Tk()
root.title("Widget-Level Validation")
root.geometry("490x120")

# Add widgets
name_label = ttk.Label(root, text="Name:")
name_label.grid(row=0, column=0, padx=5, pady=5)
name_entry = ttk.Entry(root, width=35)
name_entry.grid(row=0, column=1, padx=5, pady=5)
age_label = ttk.Label(root, text="Age:")
age_label.grid(row=1, column=0, padx=5, pady=5)
age_entry = ttk.Entry(root, width=35)
age_entry.grid(row=1, column=1, padx=5, pady=5)
label = ttk.Label(root, text="Display")
label.grid(row=2, column=0, columnspan=2, padx=5, pady=5)

# Run the app's main loop
root.mainloop()

In this example, we are creating a window with two entry widgets and a label for displaying a message after the validation process.

If you run this example, you'll get the following window:

Window showing two Entry boxes with a validation message Window showing two Entry boxes with a validation message

In this next example, we'll validate the age input when the user moves the focus out of the age entry. First, let's create the function for validating input:

python
from tkinter import Tk, ttk

# Create the app's main window
root = Tk()
root.title("Widget-Level Validation")
root.geometry("490x120")


# Create a validation function
def validate_age():
    age = age_entry.get()
    if age:
        if age.isdigit() and int(age) in range(1, 151):
            label.config(
                text=f"Valid age: {age}",
                foreground="green",
            )
            return True
        else:
            label.config(
                text="Age must be a number between 1 and 150",
                foreground="red",
            )
            return False
    else:
        label.config(
            text="Entry is empty",
            foreground="red",
        )
        return False

# ...

The validate_age() function gets the input using the get() method, storing the value in the variable age.

We again use an if statement to check whether the input value is empty or not. If the entry is empty, the else clause runs, showing the failure message.

If the user has provided a value, then we validate the input data using the isdigit() method and the built-in range() function.

IMPORTANT: It's important to note that functions used for widget-level validation must be predicate functions, returning either True or False.

Finally, we need to bind this function to the entry widget which input we want to validate. To do this, we need to pass two new arguments to the Entry() constructor:

python
# Add widgets
# ...
age_entry = ttk.Entry(
    root,
    width=35,
    validatecommand=validate_age,
    validate="focusout"
)
# ...

The validatecommand argument specifies the function to be called for input validation, and the validate command specifies on which event this function should be called. In this example, we have used "focusout". However, the validate argument can take any of the following values:

  • "focus" triggers the validation whenever the widget receives or loses focus.
  • "focusin" triggers the validation whenever the widget receives focus.
  • "focusout" triggers the validation whenever the widget loses focus.
  • "key" triggers the validation whenever any keystroke changes the widget's content.
  • "all" triggers the validation in all the above situations.
  • "none" turns the validation off.

Go ahead and run your program. Enter a name and play with different input values for the age. After entering the age value, move the focus to the name entry to trigger the validation. Here's how the app works with a valid age:

Window showing validation of a valid age Window showing validation of a valid age

When you move the focus to the Name entry, the age entry triggers the age validation function. Now let's try an invalid age:

Window showing validation of an invalid age Window showing validation of an invalid age

The validation works as expected! Whenever you enter an invalid age value and move the focus out of the age entry, you get an error message pointing out the problem. That's great!

Conclusion

In this tutorial, you've learned about input validation in GUI applications. You now know how to validate user input in Tkinter using two different validation strategies: form-level validation and widget-level validation.

You can now apply these two strategies to validate user input in your own applications.

PyQt vs. Tkinter: Which Should You Choose for Your Next Python GUI?

Graphical User Interfaces (GUIs) allow users to interact with software through intuitive and user-friendly graphical elements such as buttons, icons, text boxes, and windows. GUIs provide a simple way to perform complex tasks and navigate a software system using the mouse and keyboard and without needing to remember complex commands. By using familiar visual concepts across different tools and platforms, GUIs help make new software feel intuitive and user-friendly, even for beginners.

While Python is more commonly used for command-line tools, data science, and web apps, it is also perfectly capable of building graphical desktop applications. The Python ecosystem makes it possible to build almost anything, from small user-friendly interfaces for your scripts to more complex data analysis or engineering tools. Whether you're a Python beginner or a seasoned developer, learning how to build GUI apps with Python is an excellent addition to your skill set.

An Introduction to Python GUI Libraries in Python

One of Python's core strengths is the rich ecosystem of libraries and frameworks available. GUI programming libraries are not an exception -- you'll find several GUI libraries for Python.

While having multiple choice is great, it means that if you want to learn how to write GUI applications with Python, the first question you will need to answer is -- Which library should I use?

Even though many GUI libraries are available, none has the widespread adoption of Tkinter and PyQt. So, if you're starting with GUI programming in Python, it'll make sense to start with one of these. There are far more tutorials, help & resources available to get you up and running, and you're more likely to find help if you need it.

Google Trends Plot Ranking Tkinter, PyQt, and Other Python GUI Libraries Google Trends Plot Ranking Tkinter, PyQt, and Other Python GUI Libraries

The popularity of Tkinter largely stems from it being bundled with Python and, therefore, being the default Python GUI library. Most beginners will find this library first.

PyQt is often seen as the next logical step in your GUI journey when you want to start building real applications or commercial-quality software with Python.

Whether you choose Tkinter or PyQt will largely depend on your goals for writing GUI applications.

In this article, we'll explore and compare Tkinter and PyQt. We'll assess their pros & cons for different use cases and help you decide which library to use for your project.

The Tkinter GUI Library

Best for simple tool GUIs, small portable applications.

Tkinter is the default GUI library for Python. It comes bundled with Python on both Windows and macOS. On Linux, you may have to install additional packages to get it set up.

The library is a wrapper around the Tcl/Tk GUI toolkit. Its name is an amalgamation of the words Tk and Interface.

Tkinter supports standard layouts and basic widgets, as well as more complex widgets, such as tabbed views & progress bars. Tkinter is a pure GUI library rather than a GUI framework. It only provides a set of graphical components for building GUIs.

It doesn't provide support for GUIs-driven data sources, databases, or for displaying or manipulating multimedia or hardware. However, if you need to make something simple that doesn't require any additional dependencies, Tkinter may be what you are looking for.

Tkinter is a cross-platform GUI library. It was first released in 1990, and it has continued to evolve until today. For example, the addition of the Themed Tk extensions in 2009 improved the appearance of widgets, giving a native look & feel to your Tkinter GUIs.

Tkinter is a limited library with a friendly API (application programming interface) that makes it easy to understand and learn. Because of this, Tkinter tends to be the first choice for creating quick GUIs for relatively small Python programs.

To learn more about how to use Tkinter, check out Build your own desktop apps with Python & Tkinter.

How Do You Install Tkinter?

Tkinter comes installed by default with Python on macOS and Windows. On some Linux systems, you may need to install an additional package. Tkinter also runs fine on a Raspberry Pi, although depending on your distribution, you may need to install it first.

How Can You Write a Tkinter App?

A minimal Hello, World! GUI application in Tkinter will look something like this:

python
import tkinter as tk

# Create the app's main window
window = tk.Tk()
window.title("Hello, World!")

def handle_button_press():
    window.destroy()

button = tk.Button(text="My simple app.", command=handle_button_press)
button.pack()

# Start the event loop
window.mainloop()

In this example, we first import tkinter as tk, a common practice when using Tkinter. We then create our app's main window by instantiating the Tk class. The title() method allows us to give a descriptive title to the app's windows. Next we write a function to responding to a click on the button. In this case our method closes the application window & terminates it.

To create the button, we use the Button class with an appropriate text. In order to trigger our handle_button_press() function when the button is pressed, we pass the function to Button as the command argument.

Next, we call pack() on our button object. This organizes our widget into the window layout -- although in this case, there is only a single widget. Finally, we run the app by calling mainloop() on our window object.

If you run the example, you'll see a simple window like follows.

A Tkinter App Using Tk Standard Widgets A Tkinter App Using Tk Standard Widgets

As you can see, by default, Tkinter has very rudimentary-looking widgets.

We can modify the example to use Themed Tk widgets, which give a native appearance to widgets. The changes are minor, adding a from tkinter import ttk import and using ttk.<Widget> when constructing widgets.

python
import tkinter as tk
from tkinter import ttk

# Create the app's main window
window = tk.Tk()
window.title("Hello, World!")

def handle_button_press():
    window.destroy()

button = ttk.Button(text="My simple app.", command=handle_button_press)
button.pack()

# Start the event loop
window.mainloop()

If you run this example, the simple window will appear again but now using platform-native widgets (what you see will depend on your own system).

A Tkinter App Using Themed Tk Widgets A Tkinter App Using Themed Tk Widgets

What Is Tkinter Commonly Used for?

Tkinter is typically used for simple GUIs. For example, small proof-of-concept, research, or educational apps built by Python hobbyists, and the like. The non-native appearance of Tkinter apps and lack of a real application framework make this library unsuitable for building professional applications, particularly commercial software.

While it's possible to put something simple together with Tkinter, as your application grows or your requirements become more complex, you'll likely end up spending more time reinventing the wheel than you saved by using a "simple" system. So, if you expect your project to grow in the future, then you should start with PyQt instead.

Is Tkinter Outdated?

The complaints of Tkinter being outdated largely stem from how the apps built with the library look on modern systems compared to other apps. Another important complaint driver is the library's limited set of widgets.

Tk, the library around which Tkinter was built, was first released in 1991. However, it's been continuously developed & maintained. In 2009, Tkinter added support for Tk 8.5 "Themed Tk" (Ttk), which allows Tk widgets to be more easily themed to look like the native on different desktop environments. Ttk also added some additional widgets, such as combo boxes, progress bars, tree views, notebooks, separators, and size grips, which weren't available in default Tk.

Why Does Tkinter Look Old-Fashioned?

It doesn't have to! Tkinter's reputation for looking bad stems from earlier versions of Tk without the native platform-theming support. These early versions used an old cross-platform "Motif" style, which was blocky and a bit ugly.

Tk 8.5 added support for desktop native theming, so your applications now follow the style of the desktop they are running on. This new look is provided through Themed Tk, which was added to Tkinter in 2009.

If you don't want a native appearance but still want to improve how your Tkinter applications look, there are other options too. For example, Tkinter Designer allows you to design graphical interfaces using Figma. Once you have the interface ready, then you can export it into a working Tkinter app.

How Can You Design GUIs for Tkinter?

There isn't an official design tool for Tkinter GUIs. However, you can find some tools available online. For example, Visual Tk allows you to build a GUI using a drag-drop interface in your browser & will then generate the Python code for you to create the interface in Tkinter itself.

Alternatively, Tkinter Designer will take a design drawn in the Figma software and use it to build a Tkinter-based GUI.

What Are Tkinter's Pros & Cons?

You can find plenty of reasons to consider Tkinter for your next project and reasons you may want to try something else. Let's take a look at some of the pros & cons of using Tkinter to build GUI applications with Python so that you can be in a better position to decide.

The pros of using Tkinter include the following:

  • It is part of the Python standard library and comes installed by default (on Windows and macOS). Once you install Python, you're ready to start building GUIs with Tkinter.
  • It doesn't have additional dependencies for your applications unless you need third-party libraries for additional functionality.
  • It is relatively simple, meaning there isn't much to take in while learning to use it.
  • It is a cross-platform library
  • It has a lot of documentation and tutorials available.
  • It can be used freely for commercial software because its license is the same as Python's.

On the other hand, the cons of using Tkinter include the following:

  • It doesn't come with advanced widgets, such as data-driven views, database interfaces, vector graphics canvas, or multimedia GUI elements. To implement any of these in your applications, you need to write them yourself. This is achievable but will add to your development & maintenance burden.
  • It has no official GUI designer application. You'll find a few third-party options available with varying degrees of completeness and flexibility.
  • It lacks bundled features, which means that you're more likely to need third-party libraries to complete your applications. Luckily, there is no shortage of those in the Python ecosystem!
  • It doesn't have a native look & feel by default. The default appearance of Tkinter applications is old-fashioned. However, this is largely fixed by the Themed Tk extensions.

Now that you have a better idea of what Tkinter is and what are its main features and limitations, it's time for you to know its closest competitor, PyQt.

The PyQt GUI Framework

Best for desktop applications, multimedia, scientific, and engineering software.

PyQt is a Python GUI framework built around the C++ Qt framework, which is developed and maintained by the Qt Company. It allows us to create modern interfaces that look right at home on any platform, including Windows, macOS, Linux, and even Android. It also has solid tooling, with the most notable being Qt Creator, which includes a WYSIWYG editor for designing GUI interfaces quickly and easily.

PyQt is developed and maintained by Riverbank Computing and was first released in 1998, four years after Tkinter.

The free-to-use version of PyQt is licensed under GNU General Public License (GPL) v3. This means that PyQt is limited to GPL-licensed applications unless you purchase its commercial license.

The Qt Company has its own Python binding for Qt, which is called PySide. This library was released in 2009. The main difference between PyQt and PySide is in licensing. PySide is licensed under GNU Lesser General Public License (LGPL), which means that you use PySide in non-GPL applications without any additional fee.

Qt, and by extension PyQt, is not just a GUI library. It's a complete GUI application development framework. In addition to standard GUI components, such as widgets and layouts, Qt provides:

  • MVC (model-view-controller) data-driven views (spreadsheets, tables)
  • Database interfaces and models
  • Graph plotting
  • Vector graphics visualization
  • Multimedia playback
  • Sound effects and playlists
  • Interfaces for hardware, such as printing devices

Qt's signals and slots mechanism for event programming allows us to properly architect complex applications from reusable and isolated components.

To learn more about how to use PyQt, check out Build your own desktop apps with Python & PyQt6.

While other toolkits can work great when building small Python tools, PyQt really comes into its own for building commercial-quality applications where we benefit from the pre-built components. These benefits come at the expense of a steep learning curve but don't feel overwhelmed yet, you can just focus on the parts your project needs.

PyQt-based applications use platform-native widgets to ensure that they look and feel native on Windows, macOS, and Qt-based Linux desktop environments.

How Do You Install PyQt?

PyQt v6.4.2 is the latest version of the library. To run it on your computer, make sure you have Python v3.6.1 or later installed. To install it, just run the following command:

sh
$ python -m pip install pyqt6

This command will install the PyQt6 library for your platform and your version of Python. The library will be automatically downloaded from PyPI.

If you want to use Qt's own official Python library, you can install PySide with python -m pip install pyside6

As of writing, only PyQt5 is currently supported on Raspberry Pi. But you can use both the Qt Widgets (standard) and QML/Qt Quick (declarative) APIs. You can use QML to build modern touchscreen interfaces with animations, transitions, and effects.

To install PyQt5 on a Raspberry Pi, run the following command:

sh
$ sudo apt-get install python3-pyqt5

This command will install PyQt5 from your Raspberry Pi current repository. Note that the Qt framework will also be installed as a dependency.

For other installation options, see our complete installation guides.

How Can You Write a PyQt App?

A minimal Hello, World! GUI application in PyQt6, using the Qt Widgets is shown below:

python
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton

# Create the app's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Hello, World!")

        button = QPushButton("My simple app.")
        button.pressed.connect(self.close)

        self.setCentralWidget(button)
        self.show()

# Create the app, the main window, and run the app
app = QApplication([])
window = MainWindow()
app.exec()

In this example, we first import the required classes from PyQt6.QtWidgets. We then define the MainWindow class, which will provide the app's main window. The window will have a title and a button. This button is connected to the close() method inherited from QMainWindow.

Finally, we instantiated QApplication and MainWindow. To run the application, we call the exec() method on the app instance.

When run, you'll get a window with the title "Hello, World!" and containing a single push button that says "My simple app."

A Windowed App With a Push Button A Windowed App With a Push Button

That's a very simple example. To showcase the real capabilities of PyQt and the Qt framework, below is a more complex example consisting of a minimal but working web browser:

python
from PyQt6.QtCore import QUrl
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.browser = QWebEngineView()
        self.browser.setUrl(QUrl("https://www.google.com"))

        self.setCentralWidget(self.browser)

        self.show()

app = QApplication([])
window = MainWindow()
app.exec()

To make this code work, you need the Qt WebEngine extensions. This is a core part of Qt's library but is installed separately due to its size. Run the following to install this from the command line.

bash
pip install PyQt6-WebEngine

With PyQt6-WebEngine installed, you can now use the QWebEngineView class in your own applications.

If you run the code above, you'll get a minimal web browser that loads up Google & lets you browse from there.

A Minimal Web Browser, Written in PyQt A Minimal Web Browser, Written in PyQt

For a final example, we'll create a quick video player. This example uses layouts to build a simple GUI with a viewer and a button. You can press the button to launch a platform native file picker dialog:

python
from PyQt6.QtCore import QSize, QUrl
from PyQt6.QtMultimedia import QMediaPlayer
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtWidgets import (
    QApplication,
    QFileDialog,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Video Player")

        self.viewer = QVideoWidget()
        self.viewer.setMinimumSize(QSize(800, 400))
        self.player = QMediaPlayer()

        self.loadVideoBtn = QPushButton("Open video file...")
        self.loadVideoBtn.pressed.connect(self.openVideFile)

        layout = QVBoxLayout()
        layout.addWidget(self.viewer)
        layout.addWidget(self.loadVideoBtn)
        self.setLayout(layout)

    def openVideFile(self):
        filename, _ = QFileDialog.getOpenFileName(
            self,
            caption="Open video file",
            filter="MP4 Video (*.mp4)",
        )

        if filename:
            video = QUrl(filename)
            self.player.setSource(video)
            self.player.setVideoOutput(self.viewer)
            self.player.play()

app = QApplication([])
window = Window()
window.show()
app.exec()

When you run this example you'll get a very basic video player that's capable of loading and playing videos in MP4 format.

A Minimal MP4 Video Player, Written in PyQt A Minimal MP4 Video Player, Written in PyQt

The above examples should give you an idea of how powerful PyQt actually is. As you can see, there is much more to it than just a set of simple GUI widgets.

For some more demo PyQt applications, see our library of examples.

What Is PyQt Commonly Used for?

PyQt is most commonly used for, and particularly well suited for, building full-featured GUI desktop applications. As you already learned, PyQt supports (MVC-like) data-driven views, vector graphics, animations & transitions, databases, and threading/concurrency.

Qt Designer, the GUI creator provided by Qt, allows you to build professional quality software in no time. The signals and slots mechanism makes it possible to properly decouple the components of an application, allowing for robust and maintainable system architectures.

You can also use PyQt for building touchscreen user interfaces for Raspberry Pi-powered hardware -- both using the Qt Widgets and QML/Qt Quick APIs. While PyQt can technically be used to build apps for mobile devices, this type of apps is rarely seen outside of the hobbyist space.

Can You Use PyQt for Open-Source and Commercial Apps?

Yes, you can! PyQt is free to use for personal development and GPL-licensed open-source software. For non-GPL software, such as commercially licensed software, a license is required from Riverbank Computing.

The Qt for Python framework ---PySide--- by the Qt Company is licensed under the LGPL. If you use PySide, then you don't need to license your software under the GPL (or LGPL). As a result, you don't have to share your application's source code. So, if you don't want to buy a commercial license for PyQt, then you can use PySide, which doesn't require a license for use in commercial software.

How Can You Design GUIs for PyQt Apps?

The Qt framework provides Qt Designer, which is a drag-drop UI editor. You can use Qt Designer to design modern and intuitive interfaces for your PyQt applications quickly. The interfaces generated using Qt Designer can be either loaded as-is in your applications or converted to Python code that you can then import into your apps.

On Windows, you can install Qt Designer and other Qt tools by using pip as follows:

bash
$ python -m pip install pyqt6-tools

This command installs Qt Designer from PyPI. To run the GUI editor, you can execute the designer.exe app from your Script directory.

If you use PySide, then Qt Designer will be installed by default with the library.

Finally, you can also build GUIs from scratch using Python code. Whether you use Qt Designer or code is entirely up to you. The best choice will largely depend on your project.

What Are PyQt's Pros and Cons?

There are a number of reasons you may want to choose PyQt for your project and reasons you may not. Let's take a look at some of the pros and cons of using PyQt to build GUI applications with Python.

To kick things off, let's start with the pros:

  • It is a powerful & modern GUI framework that is suitable for building professional applications.
  • It includes several advanced widgets, including data-driven views, charts, database interfaces, vector graphics canvas, video playback, and a fully-functional web browser component.
  • It can take advantage of Qt Designer, which allows you to design GUIs using a graphical drag-and-drop editor.
  • It is cross-platform and can run on Windows, Linux, macOS, and mobile devices.
  • It provides modern and native-looking GUI components out of the box in all the major platforms. These components can be largely customized if required.
  • It is a batteries-included library, which means that you can accomplish many things with PyQt directly. This characteristic means less need for third-party dependencies.
  • It has plenty of support and online learning resources, including our own complete PyQt tutorial.
  • It allows the creation of touchscreen interfaces with the QML/Qt Quick API.

Even though PyQt has many neat features and advantages, it also has some cons:

  • It can be intimidating to beginners. The size of the library and its complex feature set can make it overwhelming, to begin with.
  • It has poor and incomplete Python documentation. As an alternative, you can use the official Qt documentation. However, this documentation is for the C++ library and can be hard to translate.
  • It will take time to fully learn the framework and how it works.
  • It allows for a GPL or commercial license only.

Up to this point, you've learned a lot about Tkinter and PyQt. To make your life more pleasant, the following section provides a quick and summarized comparison between these two GUI tools.

Tkinter vs. PyQt: A Feature Comparison

Now that we have a good understanding of Tkinter and PyQt, let's put them head-to-head on some key features:

Feature Tkinter PyQt
Installation & Setup It's part of the Python installer on Windows and macOS. It may require additional packages on Linux. It needs to be installed separately using pip. It can also be installed from the source.
License It uses the Python license. It uses GPL or a commercial license.
GUI Builder Tools It has no standard GUI builder app, but some third-party tools are available. It can use Qt Designer for building GUIs.
Widgets Set It has a basic set of widgets for most common applications. It has basic and advanced widgets for building quite complex interfaces.
Application framework It's not a framework, so you'll have to use third-party Python libraries to implement many features. It's a full GUI framework that includes databases, plotting, vector graphics, threading, multimedia, networking, and more.

As you can see, PyQt is the most feature-rich of the two libraries. It is more capable of both building the GUI and the backend of your application. That said, if you're building simple GUI tools, then Tkinter may still be more than enough. It'll all depend on your project.

Decision Time: How to Choose the Best GUI Library for Your Project

So far, we've looked in detail at each library, seen some example code, and explored the pros and cons of using each library. We've also addressed some common questions that come up when looking at the two libraries.

By now, you have everything you need to decide which is best for your project. However, you may still be stuck on making the decision. In the following sections, we'll recap what we've learned about PyQt and Tkinter in the context of your own project and goals.

What Are Your Goals?

Which GUI library you pick is heavily dependent on your goals in writing GUI applications. If you are learning Python and just want to experiment, then Tkinter is a perfectly decent way to do that. On the other hand, if you are learning GUI development with a view to building professional applications, then it will make more sense to start with PyQt.

Qt, which PyQt is built on, is a complete GUI application framework that provides tools and components for building modern applications. This feature offloads a lot of the manual and repetitive work, helping you build well-architected systems.

Take, for example, loading a CSV file and displaying it in a table. In PyQt, you can write a small model to interface between the data source and the built-in table view. PyQt does all the work for you. It takes the data and displays it in an efficient way.

In Tkinter, you will need to create the table by yourself, widget by widget, in a grid layout. Then, you would have to use some external tool for loading the data from the CSV file. Finally, you would have to find a way to display the data in your custom Tkinter table.

In PyQt, you can have millions of rows in a table without problems using a single widget created to render the entire table. In Tkinter, each cell will be an individual widget object ---even those outside the view--- meaning you'll quickly encounter problems if you work with large data.

Do You Need a GUI Library or a GUI Framework?

Tkinter is a GUI library, while PyQt is a GUI framework. While both allow you to build graphical user interfaces, they differ in the scope of what they include and what parts of your application they help you build.

While a GUI library will help you add, position, and draw widgets in your application's GUI and hook those widgets up to your own code, a GUI framework provides additional functionalities, which are commonly required when building applications.

For example, PyQt provides components for connecting to databases and creating semi-automatic model-views of database entries. It provides a vector graphics canvas, plotting, 3D rendering, networking, threading/concurrency, and more.

Even though many of these features are already available in the Python standard library or in third-party libraries, most of them will not integrate as cleanly into a GUI program as those provided natively by the framework itself.

The trade-off is between using a single, big dependency (PyQt) vs. lots of small dependencies (standard-library or third-party) to build your apps. Using a framework can speed up the development of complex projects because much of what you need is available out of the box. As mentioned, whether you get any benefit from the PyQt framework will largely depend on your specific project.

Is Tkinter Easier to Learn Than PyQt?

It can be. Previously, there was a lack of beginner tutorials available for PyQt. This condition made it difficult to get started. That's no longer the case. Now you'll find lots of PyQt5 and PyQt6 tutorials available online.

Our own PyQt6 tutorial takes you from the absolute basics of GUI development concepts to building relatively complex applications. We're adding new tutorials regularly, covering both basic and advanced topics.

There are more beginner-friendly tutorials available for Tkinter than PyQt. Basic Tkinter examples tend to avoid using subclasses, while PyQt examples default to using them, which can imply more reasoning if you're not familiar with object-oriented programming and inheritance.

PyQt introduces additional and complex concepts, such as signals and slots, which can be confusing for new developers. However, they are one of the most powerful parts of the framework, making it possible to build well-architected and maintainable software. The time taken to understand these things is well rewarded.

Which One Should You Learn First, Tkinter or PyQt?

There is little benefit in starting with Tkinter if you plan to switch to PyQt later. While PyQt is a large framework with thousands of classes and features, you don't need to learn all of it at once. While some of the basic GUI concepts you learn with Tkinter -- widgets, layouts, event-based programming -- will transfer over to PyQt, there are many other concepts that won't.

As a developer, you'll be looking for tools that are easy to use, well-designed for the job, and at the same time, can grow with your projects. If you want to move on to develop professional or commercial software with Python, you don't want to start over again with an entirely new stack. If you think you are likely to want to migrate to PyQt later, then you may as well start there first.

What Next?

Now you have enough knowledge to make an informed decision between using PyQt or Tkinter for your GUI projects. On Python GUIs, we have lots of tutorials to help you take the next step of actually building something great!

Our PyQt6 tutorial is pretty complete and takes you from basic examples to complex applications. This way, you'll get the required skills to build multimedia, scientific, and engineering software at a professional level.

On the other hand, if you're just looking to create simple GUIs for your automation and utility tools, then our Tkinter tutorial will give you the basics you need to get a window on the screen and start experimenting.

In any case, you will have fun building GUIs!

For an in-depth guide to building GUIs with Python see my PyQt6 book.

Working With Git and Github in Your Python Projects

Using a version control system (VCS) is crucial for any software development project. These systems allow developers to track changes to the project's codebase over time, removing the need to keep multiple copies of the project folder.

VCSs also facilitate experimenting with new features and ideas without breaking existing functionality in a given project. They also enable collaboration with other developers that can contribute code, documentation, and more.

In this article, we'll learn about Git, the most popular VCS out there. We'll learn everything we need to get started with this VCS and start creating our own repositories. We'll also learn how to publish those repositories to GitHub, another popular tool among developers nowadays.

Installing and Setting Up Git

To use Git in our coding projects, we first need to install it on our computer. To do this, we need to navigate to Git's download page and choose the appropriate installer for our operating system. Once we've downloaded the installer, we need to run it and follow the on-screen instructions.

We can check if everything is working correctly by opening a terminal or command-line window and running git --version.

Once we've confirmed the successful installation, we should provide Git with some personal information. You'll only need to do this once for every computer. Now go ahead and run the following commands with your own information:

shell
$ git config --global user.name <"YOUR NAME">
$ git config --global user.email <name@email.com>

The first command adds your full name to Git's config file. The second command adds your email. Git will use this information in all your repositories.

If you publish your projects to a remote server like GitHub, then your email address will be visible to anyone with access to that repository. If you don't want to expose your email address this way, then you should create a separate email address to use with Git.

As you'll learn in a moment, Git uses the concept of branches to manage its repositories. A branch is a copy of your project's folder at a given time in the development cycle. The default branch of new repositories is named either master or main, depending on your current version of Git.

You can change the name of the default branch by running the following command:

shell
$ git config --global init.defaultBranch <branch_name>

This command will set the name of Git's default branch to branch_name. Remember that this is just a placeholder name. You need to provide a suitable name for your installation.

Another useful setting is the default text editor Git will use to type in commit messages and other messages in your repo. For example, if you use an editor like Visual Studio Code, then you can configure Git to use it:

shell
# Visual Studio Code
$ git config --global core.editor "code --wait"

With this command, we tell Git to use VS Code to process commit messages and any other message we need to enter through Git.

Finally, to inspect the changes we've made to Git's configuration files, we can run the following command:

shell
$ git config --global -e

This command will open the global .gitconfig file in our default editor. There, we can fix any error we have made or add new settings. Then we just need to save the file and close it.

Understanding How Git Works

Git works by allowing us to take a snapshot of the current state of all the files in our project's folder. Each time we save one of those snapshots, we make a Git commit. Then the cycle starts again, and Git creates new snapshots, showing how our project looked like at any moment.

Git was created in 2005 by Linus Torvalds, the creator of the Linux kernel. Git is an open-source project that is licensed under the GNU General Public License (GPL) v2. It was initially made to facilitate kernel development due to the lack of a suitable alternative.

The general workflow for making a Git commit to saving different snapshots goes through the following steps:

  1. Change the content of our project's folder.
  2. Stage or mark the changes we want to save in our next commit.
  3. Commit or save the changes permanently in our project's Git database.

As the third step mentions, Git uses a special database called a repository. This database is kept inside your project's directory under a folder called .git.

Version-Controlling a Project With Git: The Basics

In this section, we'll create a local repository and learn how to manage it using the Git command-line interface (CLI). On macOS and Linux, we can use the default terminal application to follow along with this tutorial.

On Windows, we recommend using Git Bash, which is part of the Git For Windows package. Go to the Git Bash download page, get the installer, run it, and follow the on-screen instruction. Make sure to check the Additional Icons -> On the Desktop to get direct access to Git Bash on your desktop so that you can quickly find and launch the app.

Alternatively, you can also use either Windows' Command Prompt or PowerShell. However, some commands may differ from the commands used in this tutorial.

Initializing a Git Repository

To start version-controlling a project, we need to initialize a new Git repository in the project's root folder or directory. In this tutorial, we'll use a sample project to facilitate the explanation. Go ahead and create a new folder in your file system. Then navigate to that folder in your terminal by running these commands:

shell
$ mkdir sample_project
$ cd sample_project

The first command creates the project's root folder or directory, while the second command allows you to navigate into that folder. Don't close your terminal window. You'll be using it throughout the next sections.

To initialize a Git repository in this folder, we need to use the git init command like in the example below:

shell
$ git init
Initialized empty Git repository in /.../sample_project/.git/

This command creates a subfolder called .git inside the project's folder. The leading dot in the folder's name means that this is a hidden directory. So, you may not see anything on your file manager. You can check the existence of .git with the ls -a, which lists all files in a given folder, including the hidden ones.

Checking the Status of Our Project

Git provides the git status command to allow us to identify the current state of a Git repository. Because our sample_project folder is still empty, running git status will display something like this:

shell
$ git status
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)

When we run git status, we get detailed information about the current state of our Git repository. This command is pretty useful, and we'll turn back to it in multiple moments.

As an example of how useful the git status command is, go ahead and create a file called main.py inside the project's folder using the following commands:

shell
$ touch main.py

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    main.py

nothing added to commit but untracked files present (use "git add" to track)

With the touch command, we create a new main.py file under our project's folder. Then we run git status again. This time, we get information about the presence of an untracked file called main.py. We also get some basic instructions on how to add this file to our Git repo. Providing these guidelines or instructions is one of the neatest features of git status.

Now, what is all that about untracked files? In the following section, we'll learn more about this topic.

Tracking and Committing Changes

A file in a Git repository can be either tracked or untracked. Any file that wasn't present in the last commit is considered an untracked file. Git doesn't keep a history of changes for untracked files in your project's folder.

In our example, we haven't made any commits to our Git repo, so main.py is naturally untracked. To start tracking it, run the git add command as follows:

shell
$ git add main.py

$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   main.py

This git add command has added main.py to the list of tracked files. Now it's time to save the file permanently using the git commit command with an appropriate commit message provided with the -m option:

shell
$ git commit -m "Add main.py"
[main (root-commit) 5ac6586] Add main.py
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 main.py

$ git status
On branch master
nothing to commit, working tree clean

We have successfully made our first commit, saving main.py to our Git repository. The git commit command requires a commit message, which we can provide through the -m option. Commit messages should clearly describe what we have changed in our project.

After the commit, our main branch is completely clean, as you can conclude from the git status output.

Now let's start the cycle again by modifying main.py, staging the changes, and creating a new commit. Go ahead and run the following commands:

shell
$ echo "print('Hello, World!')" > main.py
$ cat main.py
print('Hello, World!')

$ git add main.py

$ git commit -m "Create a 'Hello, World!' script  on  main.py"
[main 2f33f7e] Create a 'Hello, World!' script  on  main.py
 1 file changed, 1 insertion(+)

The echo command adds the statement "print('Hello, World!')" to our main.py file. You can confirm this addition with the cat command, which lists the content of one or more target files. You can also open main.py in your favorite editor and update the file there if you prefer.

We can also use the git stage command to stage or add files to a Git repository and include them in our next commit.

We've made two commits to our Git repo. We can list our commit history using the git log command as follows:

shell
$ git log --oneline
2f33f7e (HEAD -> main) Create a 'Hello, World!' script  on  main.py
5ac6586 Add main.py

The git log command allows us to list all our previous commits. In this example, we've used the --oneline option to list commits in a single line each. This command takes us to a dedicated output space. To leave that space, we can press the letter Q on our keyboard.

Using a .gitignore File to Skip Unneeded Files

While working with Git, we will often have files and folders that we must not save to our Git repo. For example, most Python projects include a venv/ folder with a virtual environment for that project. Go ahead and create one with the following command:

shell
$ python -m venv venv

Once we've added a Python virtual environment to our project's folder, we can run git status again to check the repo state:

shell
$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    venv/

nothing added to commit but untracked files present (use "git add" to track)

Now the venv/ folder appears as an untracked file in our Git repository. We don't need to keep track of this folder because it's not part of our project's codebase. It's only a tool for working on the project. So, we need to ignore this folder. To do that, we can add the folder to a .gitignore file.

Go ahead and create a .gitignore file in the project's folder. Add the venv/ folders to it and run git status:

shell
$ touch .gitignore
$ echo "venv/" > .gitignore
$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    .gitignore

nothing added to commit but untracked files present (use "git add" to track)

Now git status doesn't list venv/ as an untracked file. This means that Git is ignoring that folder. If we take a look at the output, then we'll see that .gitignore is now listed as an untracked file. We must commit our .gitignore files to the Git repository. This will prevent other developers working with us from having to create their own local .gitignore files.

We can also list multiple files and folders in our .gitignore file one per line. The file even accepts glob patterns to match specific types of files, such as *.txt. If you want to save yourself some work, then you can take advantage of GitHub's gitignore repository, which provides a rich list of predefined .gitignore files for different programming languages and development environments.

We can also set up a global .gitignore file on our computer. This global file will apply to all our Git repositories. If you decide to use this option, then go ahead and create a .gitignore_global in your home folder.

Working With Branches in Git

One of the most powerful features of Git is that it allows us to create multiple branches. A branch is a copy of our project's current status and commits history. Having the option to create and handle branches allows us to make changes to our project without messing up the main line of development.

We'll often find that software projects maintain several independent branches to facilitate the development process. A common branch model distinguishes between four different types of branches:

  1. A main or master branch that holds the main line of development
  2. A develop branch that holds the last developments
  3. One or more feature branches that hold changes intended to add new features
  4. One or more bugfix branches that hold changes intended to fix critical bugs

However, the branching model to use is up to you. In the following sections, we'll learn how to manage branches using Git.

Creating New Branches

Working all the time on the main or master branch isn't a good idea. We can end up creating a mess and breaking the code. So, whenever we want to experiment with a new idea, implement a new feature, fix a bug, or just refactor a piece of code, we should create a new branch.

To kick things off, let's create a new branch called hello on our Git repo under the sample_project folder. To do that, we can use the git branch command followed by the branch's name:

shell
$ git branch hello
$ git branch --list
* main
  hello

The first command creates a new branch in our Git repo. The second command allows us to list all the branches that currently exist in our repository. Again, we can press the letter Q on our keyboard to get back to the terminal prompt.

The star symbol denotes the currently active branch, which is main in the example. We want to work on hello, so we need to activate that branch. In Git's terminology, we need to check out to hello.

Checking Out to a New Branch

Although we have just created a new branch, in order to start working on it, we need to switch to or check out to it by using the git checkout command as follows:

shell
$ git checkout hello
Switched to branch 'hello'

$ git branch --list
  main
* hello

$ git log --oneline
2f33f7e (HEAD -> hello, main) Create a 'Hello, World!' script  on  main.py
5ac6586 Add main.py

The git checkout command takes the name of an existing branch as an argument. Once we run the command, Git takes us to the target branch.

We can derive a new branch from whatever branch we need.

As you can see, git branch --list indicates which branch we are currently on by placing a * symbol in front of the relevant branch name. If we check the commit history with git log --oneline, then we'll get the same as we get from main because hello is a copy of it.

The git checkout can take a -b flag that we can use to create a new branch and immediately check out to it in a single step. That's what most developers use while working with Git repositories. In our example, we could have run git checkout -b hello to create the hello branch and check out to it with one command.

Let's make some changes to our project and create another commit. Go ahead and run the following commands:

shell
$ echo "print('Welcome to PythonGUIs!')" >> main.py
$ cat main.py
print('Hello, World!')
print('Welcome to PythonGUIs!')

$ git add main.py
$ git commit -m "Extend our 'Hello, World' program with a welcome message."
[hello be62476] Extend our 'Hello, World' program with a welcome message.
 1 file changed, 1 insertion(+)

The final command committed our changes to the hello branch. If we compare the commit history of both branches, then we'll see the difference:

shell
$ git log --oneline -1
be62476 (HEAD -> hello) Extend our 'Hello, World' program with a welcome message.

$ git checkout main
Switched to branch 'main'

$ git log --oneline -1
2f33f7e (HEAD -> main) Create a 'Hello, World!' script  on  main.py

In this example, we first run git log --oneline with -1 as an argument. This argument tells Git to give us only the last commit in the active branch's commit history. To inspect the commit history of main, we first need to check out to that branch. Then we can run the same git log command.

Now say that we're happy with the changes we've made to our project in the hello branch, and we want to update main with those changes. How can we do this? We need to merge hello into main.

Merging Two Branches Together

To add the commits we've made in a separate branch back to another branch, we can run what is known as a merge. For example, say we want to merge the new commits in hello into main. In this case, we first need to switch back to main and then run the git merge command using hello as an argument:

shell
$ git checkout main
Already on 'main'

$ git merge hello
Updating 2f33f7e..be62476
Fast-forward
 main.py | 1 +
 1 file changed, 1 insertion(+)

To merge a branch into another branch, we first need to check out the branch we want to update. Then we can run git merge. In the example above, we first check out to main. Once there, we can merge hello.

Deleting Unused Branches

Once we've finished working in a given branch, we can delete the entire branch to keep our repo as clean as possible. Following our example, now that we've merged hello into main, we can remove hello.

To remove a branch from a Git repo, we use the git branch command with the --delete option. To successfully run this command, make sure to switch to another branch before:

shell
$ git checkout main
Already on 'main'

$ git branch --delete hello
Deleted branch hello (was be62476).

$ git branch --list
* main

Deleting unused branches is a good way to keep our Git repositories clean, organized, and up to date. Also, deleting them as soon as we finish the work is even better because having old branches around may be confusing for other developers collaborating with our project. They might end up wondering why these branches are still alive.

Using a GUI Client for Git

In the previous sections, we've learned to use the git command-line tool to manage Git repositories. If you prefer to use GUI tools, then you'll find a bunch of third-party GUI frontends for Git. While they won't completely replace the need for using the command-line tool, they can simplify your day-to-day workflow.

You can get a complete list of standalone GUI clients available on the Git official documentation.

Most popular IDEs and code editors, including PyCharm and Visual Studio Code, come with basic Git integration out-of-the-box. Some developers will prefer this approach as it is directly integrated with their development environment of choice.

If you need something more advanced, then GitKraken is probably a good choice. This tool provides a standalone, cross-platform GUI client for Git that comes with many additional features that can boost your productivity.

Managing a Project With GitHub

If we publish a project on a remote server with support for Git repositories, then anyone with appropriate permissions can clone our project, creating a local copy on their computer. Then, they can make changes to our project, commit them to their local copy, and finally push the changes back to the remote server. This workflow provides a straightforward way to allow other developers to contribute code to your projects.

In the following sections, we'll learn how to create a remote repository on GitHub and then push our existing local repository to it. Before we do that, though, head over to GitHub.com and create an account there if you don't have one yet. Once you have a GitHub account, you can set up the connection to that account so that you can use it with Git.

Setting Up a Secure Connection to GitHub

In order to work with GitHub via the git command, we need to be able to authenticate ourselves. There are a few ways of doing that. However, using SSH is the recommended way. The first step in the process is to generate an SSH key, which you can do with the following command:

shell
$ ssh-keygen -t ed25519 -C "GitHub - name@email.com"

Replace the placeholder email address with the address you've associated with your GitHub account. Once you run this command, you'll get three different prompts in a row. You can respond to them by pressing Enter to accept the default option. Alternatively, you can provide custom responses.

Next, we need to copy the contents of our id_ed25519.pub file. To do this, you can run the following command:

shell
$ cat ~/.ssh/id_ed25519.pub

Select the command's output and copy it. Then go to your GitHub Settings page and click the SSH and GPG keys option. There, select New SSH key, set a descriptive title for the key, make sure that the Key Type is set to Authentication Key, and finally, paste the contents of id_ed25519.pub in the Key field. Finally, click the Add SSH key button.

At this point, you may be asked to provide some kind of Two-Factor Authentication (2FA) code. So, be ready for that extra security step.

Now we can test our connection by running the following command:

shell
$ ssh -T git@github.com
The authenticity of host 'github.com (IP ADDRESS)' can not be established.
ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Make sure to check whether the key fingerprint shown on your output matches GitHub's public key fingerprint. If it matches, then enter yes and press Enter to connect. Otherwise, don't connect.

If the connection is successful, we will get a message like this:

shell
Hi USERNAME! You have successfully authenticated, but GitHub does not provide shell access.

Congrats! You've successfully connected to GitHub via SSH using a secure SSH key. Now it's time to start working with GitHub.

Creating and Setting Up a GitHub Repository

Now that you have a GitHub account with a proper SSH connection, let's create a remote repository on GitHub using its web interface. Head over to the GitHub page and click the + icon next to your avatar in the top-right corner. Then select New repository.

Give your new repo a unique name and choose who can see this repository. To continue with our example, we can give this repository the same name as our local project, sample_project.

To avoid conflicts with your existing local repository, don't add .gitignore, README, or LICENSE files to your remote repository.

Next, set the repo's visibility to Private so that no one else can access the code. Finally, click the Create repository button at the end of the page.

If you create a Public repository, make sure also to choose an open-source license for your project to tell people what they can and can't do with your code.

You'll get a Quick setup page as your remote repository has no content yet. Right at the top, you'll have the choice to connect this repository via HTTPS or SSH. Copy the SSH link and run the following command to tell Git where the remote repository is hosted:

shell
$ git remote add origin git@github.com:USERNAME/sample_project.git

This command adds a new remote repository called origin to our local Git repo.

The name origin is commonly used to denote the main remote repository associated with a given project. This is the default name Git uses to identify the main remote repo.

Git allows us to add several remote repositories to a single local one using the git remote add command. This allows us to have several remote copies of your local Git repo.

Pushing a Local Git Repository to GitHub

With a new and empty GitHub repository in place, we can go ahead and push the content of our local repo to its remote copy. To do this, we use the git push command providing the target remote repo and the local branch as arguments:

shell
$ git push -u origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (9/9), 790 bytes | 790.00 KiB/s, done.
Total 9 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:USERNAME/sample_project.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

This is the first time we push something to the remote repo sample_project, so we use the -u option to tell Git that we want to set the local main branch to track the remote main branch. The command's output provides a pretty detailed summary of the process.

Note that if you don't add the -u option, then Git will ask what you want to do. A safe workaround is to copy and paste the commands GitHub suggests, so that you don't forget -u.

Using the same command, we can push any local branch to any remote copy of our project's repo. Just change the repo and branch name: git push -u remote_name branch_name.

Now let's head over to our browser and refresh the GitHub page. We will see all of our project files and commit history there.

Now we can continue developing our project and making new commits locally. To push our commits to the remote main branch, we just need to run git push. This time, we don't have to use the remote or branch name because we've already set main to track origin/main.

Pulling Content From a GitHub Repository

We can do basic file editing and make commits within GitHub itself. For example, if we click the main.py file and then click the pencil icon at the top of the file, we can add another line of code and commit those changes to the remote main branch directly on GitHub.

Go ahead and add the statement print("Your Git Tutorial is Here...") to the end of main.py. Then go to the end of the page and click the Commit changes button. This makes a new commit on your remote repository.

This remote commit won't appear in your local commit history. To download it and update your local main branch, use the git pull command:

shell
$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 696 bytes | 174.00 KiB/s, done.
From github.com:USERNAME/sample_project
   be62476..605b6a7  main       -> origin/main
Updating be62476..605b6a7
Fast-forward
 main.py | 1 +
 1 file changed, 1 insertion(+)

Again, the command's output provides all the details about the operation. Note that git pull will download the remote branch and update the local branch in a single step.

If we want to download the remote branch without updating the local one, then we can use the [git fetch](https://git-scm.com/docs/git-fetch) command. This practice gives us the chance to review the changes and commit them to our local repo only if they look right.

For example, go ahead and update the remote copy of main.py by adding another statement like print("Let's go!!"). Commit the changes. Then get back to your local repo and run the following command:

shell
$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 731 bytes | 243.00 KiB/s, done.
From github.com:USERNAME/sample_project
   605b6a7..ba489df  main       -> origin/main

This command downloaded the latest changes from origin/main to our local repo. Now we can compare the remote copy of main.py to the local copy. To do this, we can use the git diff command as follows:

shell
$ git diff main origin/main
diff --git a/main.py b/main.py
index be2aa66..4f0e7cf 100644
--- a/main.py
+++ b/main.py
@@ -1,3 +1,4 @@
 print('Hello, World!')
 print('Welcome to PythonGUIs!')
 print("Your Git Tutorial is Here...")
+print("Let's go!!")

In the command's output, you can see that the remote branch adds a line containing print("Let's go!!") to the end of main.py. This change looks good, so we can use git pull to commit the change automatically.

Exploring Alternatives to GitHub

While GitHub is the most popular public Git server and collaboration platform in use, it is far from being the only one. GitLab.com and BitBucket are popular commercial alternatives similar to GitHub. While they have paid plans, both offer free plans, with some restrictions, for individual users.

Although, if you would like to use a completely open-source platform instead, Codeberg might be a good option. It's a community-driven alternative with a focus on supporting Free Software. Therefore, in order to use Codeberg, your project needs to use a compatible open-source license.

Optionally, you can also run your own Git server. While you could just use barebones git for this, software such as GitLab Community Edition (CE) and Forgejo provide you with both the benefits of running your own server and the experience of using a service like GitHub.

Conclusion

By now, you're able to use Git for version-controlling your projects. Git is a powerful tool that will make you much more efficient and productive, especially as the scale of your project grows over time.

While this guide introduced you to most of its basic concepts and common commands, Git has many more commands and options that you can use to be even more productive. Now, you know enough to get up to speed with Git.

Handle command-line arguments in GUI applications with PyQt6

Sometimes you want to be able to pass command line arguments to your GUI applications. For example, you may want to be able to pass files which the application should open, or change the initial startup state.

In that case it can be helpful to be able to pass custom command-line arguments to your application. Qt supports this using the QCommandLineOption and QCommandLineParser classes, the first defining optional parameters to be identified from the command line, and the latter to actually perform the parsing.

In this short tutorial we'll create a small demo application which accepts arguments on the command line.

Building the Parser

We'll start by creating an outline of our application. As usual we create a QApplication instance, however, we won't initially create any windows. Instead, we construct our command-line parser function, which accepts our app instance and uses QCommandLineParser to parse the command line arguments.

python
import sys

from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QCommandLineOption, QCommandLineParser

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()

    parser.addHelpOption()
    parser.addVersionOption()

    parser.process(app)


app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)

python
import sys

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCommandLineOption, QCommandLineParser

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()

    parser.addHelpOption()
    parser.addVersionOption()

    parser.process(app)


app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)

We don't call app.exec() yet, to start the event loop. That's because we don't have any windows, so there would be no way to exit the app once it is started! We'll create the windows in a later step.

It is important to note that you must pass sys.argv to QApplication for the command line parser to work -- the parser processes the arguments from app. If you don't pass the arguments, there won't be anything to process.

With this outline structure in place we can move on to creating our command line options.

Adding Optional Parameters

We'll add two optional parameters -- the first to allow users to toggle the window start up as maximized, and the second to pass a Qt style to use for drawing the window.

The available styles are 'windows', 'windowsvista', 'fusion' and 'macos' -- although the platform specific styles are only available on their own platform. If you use macos on Windows, or vice versa, it will have no effect. However, 'fusion' can be used on all platforms.

python
import sys

from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)



app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)
app.exec()

python
import sys

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)



app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)
app.exec()

The optional arguments are now in place. We'll now add positional arguments, which will be used to open specific files.

Adding Positional Arguments

Strictly speaking, positional arguments are any arguments not interpreted as optional arguments. You can define multiple positional arguments if you like but this is only used for help text display. You will still need to handle them yourself internally, and limit the number if necessary by throwing an error.

In our example we are specifying a single position argument file, but noting in the help text that you can provide more than one. There is no limit in our example -- if you pass more files, more windows will open.

python
import sys

from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    parser.addPositionalArgument("file", "Files to open.", "[file file file...]")

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)

    has_maximize_option = parser.isSet(maximize_option)
    app_style = parser.value(style_option)

    # Check for positional arguments (files to open).
    arguments = parser.positionalArguments()

    print("Has maximize option?", has_maximize_option)
    print("App style:", app_style)
    print("Arguments (files): ", arguments)

app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)
app.exec()

python
import sys

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    parser.addPositionalArgument("file", "Files to open.", "[file file file...]")

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)

    has_maximize_option = parser.isSet(maximize_option)
    app_style = parser.value(style_option)

    # Check for positional arguments (files to open).
    arguments = parser.positionalArguments()

    print("Has maximize option?", has_maximize_option)
    print("App style:", app_style)
    print("Arguments (files): ", arguments)

app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)
app.exec()

We've added a series of print calls to display the arguments and options extracted by Qt's QCommandLineParser. If you run the application now you can experiment by passing different arguments and seeing the result on the command line.

For example --- with no arguments.

bash
$ python command_line.py
Has maximize option? False
App style:

With -m maximize flag and a single file

bash
$ python command_line.py -m example.txt
Has maximize option? True
App style:
Arguments (files):  ['example.txt']

With a single file and using Fusion style -- there is no window yet, so this will have effect yet!

bash
$ python command_line.py -s fusion example.txt
Has maximize option? False
App style: fusion
Arguments (files):  ['example.txt']

With the argument handling in place, we can now write the remainder of the example.

Using the Arguments & Options

We'll be using a standard QPlainTextEdit widget as our file viewer. In Qt any widget without a parent is a window, so these editors will be floating independently on the desktop. If the -m flag is used we'll set these windows to be displayed maximized.

If windows are created, we'll need to start the Qt event loop to draw the windows and allow interaction with them. If no windows are created, we'll want to show the command-line help to help the user understand why nothing is happening! This will output the format of position and optional arguments that our app takes.

python
import sys

from PySide6.QtWidgets import QApplication, QPlainTextEdit
from PySide6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

windows = []  # Store references to our created windows.

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    parser.addPositionalArgument("file", "Files to open.", "[file file file...]")

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)

    has_maximize_option = parser.isSet(maximize_option)
    app_style = parser.value(style_option)

    if app_style and app_style in QT_STYLES:
        app.setStyle(app_style)

    # Check for positional arguments (files to open).
    arguments = parser.positionalArguments()

    # Iterate all arguments and open the files.
    for tfile in arguments:
        try:
            with open(tfile, 'r') as f:
                text = f.read()
        except Exception:
            # Skip this file if there is an error.
            continue

        window = QPlainTextEdit(text)

        # Open the file in a maximized window, if set.
        if has_maximize_option:
            window.showMaximized()
        # Keep a reference to the window in our global list, to stop them
        # being deleted. We can test this list to see whether to show the
        # help (below) or start the event loop (at the bottom).
        windows.append(window)

    if not windows:
        # If we haven't created any windows, show the help.
        parser.showHelp()


app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)

if windows:
    # We've created windows, start the event loop.
    app.exec()

python
import sys

from PySide6.QtWidgets import QApplication, QPlainTextEdit
from PySide6.QtCore import QCommandLineOption, QCommandLineParser

QT_STYLES = ["windows", "windowsvista", "fusion", "macos"]

windows = []  # Store references to our created windows.

def parse(app):
    """Parse the arguments and options of the given app object."""

    parser = QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()

    parser.addPositionalArgument("file", "Files to open.", "[file file file...]")

    maximize_option = QCommandLineOption(
        ["m", "maximize"],
        "Maximize the window on startup."
    )
    parser.addOption(maximize_option)

    style_option = QCommandLineOption(
        "s",
        "Use the specified Qt style, one of: " + ', '.join(QT_STYLES),
        "style"
    )
    parser.addOption(style_option)

    parser.process(app)

    has_maximize_option = parser.isSet(maximize_option)
    app_style = parser.value(style_option)

    if app_style and app_style in QT_STYLES:
        app.setStyle(app_style)

    # Check for positional arguments (files to open).
    arguments = parser.positionalArguments()

    # Iterate all arguments and open the files.
    for tfile in arguments:
        try:
            with open(tfile, 'r') as f:
                text = f.read()
        except Exception:
            # Skip this file if there is an error.
            continue

        window = QPlainTextEdit(text)

        # Open the file in a maximized window, if set.
        if has_maximize_option:
            window.showMaximized()
        # Keep a reference to the window in our global list, to stop them
        # being deleted. We can test this list to see whether to show the
        # help (below) or start the event loop (at the bottom).
        windows.append(window)

    if not windows:
        # If we haven't created any windows, show the help.
        parser.showHelp()


app = QApplication(sys.argv)
app.setApplicationName("My Application")
app.setApplicationVersion("1.0")

parse(app)

if windows:
    # We've created windows, start the event loop.
    app.exec()

The arguments are handled and processed as before however, now they actually have an effect!

Firstly, if the user passes the -s <style> option we will apply the specified style to our app instance -- first checking to see if it is one of the known valid styles.

python
if app_style and app_style in QT_STYLES:
    app.setStyle(app_style)

Next we take the list of position arguments and iterate, creating a QPlainTextEdit window and displaying the text in it. If has_maximize_option has been set, these windows are all set to be maximized with window.showMaximized().

References to the windows are stored in a global list windows, so they are not cleaned up (deleted) on exiting the function. After creating windows we test to see if this is empty, and if not show the help:

bash
$ python command_line.py
Usage: command_line.py [options] [file file file...]

Options:
  -?, -h, --help  Displays help on commandline options.
  --help-all      Displays help including Qt specific options.
  -v, --version   Displays version information.
  -m, --maximize  Maximize the window on startup.
  -s <style>      Use the specified Qt style, one of: windows, windowsvista,
                  fusion, macos

Arguments:
  file            Files to open.

If there are windows, we finally start up the event loop to display them and allow the user to interact with the application.

python
if windows:
    # We've created windows, start the event loop.
    app.exec()

Conclusion

In this short tutorial we've learned how to develop an app which accepts custom arguments and options on the command line, and uses them to modify the UI behavior. You can use this same approach in your own applications to provide command-line control over the behavior of your own applications.

For an in-depth guide to building GUIs with Python see my PyQt6 book.

CXX-Qt 0.5 Released

We just released CXX-Qt version 0.5!

CXX-Qt is a set of Rust crates for creating bidirectional Rust ⇄ C++ bindings with Qt. It can be used to integrate Rust into C++ applications using CMake or build Rust applications with Cargo. CXX-Qt provides tools for implementing QObject subclasses in Rust that can be used from C++, QML, and JavaScript.

For 0.5, the focus has mainly been on adding more common Qt types, support for Qt container types, support for pointer types, and improving inheritance. Simple QML applications can now be written entirely in Rust, including implementing Qt models, without needing any boilerplate C++ code.

Some of the more extensive developer-facing changes are listed below.

Inheritance

This release introduces new features for interfacing with C++ inheritance from Rust. These allow you to implement subclasses in Rust without needing to write boilerplate C++ code.

When subclassing some Qt classes, you need to call methods of the base class, for example QAbstractItemModel::beginInsertRows, without necessarily overriding those methods in your subclass. A new macro #[cxx_qt::inherit] can be used on extern "C++" blocks to create Rust bindings to those methods inherited from the C++ base class.

There is also an #[inherit] macro that can be used to access signals inherited from the base class. This tells CXX-Qt to skip generating a new Q_SIGNAL and to use the base class version instead.

Types

Bindings for more Qt types have been added to cxx-qt-lib:

  • QByteArray
  • QCoreApplication
  • QGuiApplication
  • QMargins
  • QMarginsF
  • QModelIndex
  • QPersistentModelIndex
  • QQmlApplicationEngine
  • QQmlEngine
  • QTimeZone
  • QStringList
  • QVector2D
  • QVector3D
  • QVector4D

We have also added support for Qt container types:

  • QList<T>
  • QVector<T>
  • QSet<T>
  • QHash<K, V>
  • QMap<K, V>

QList and QVector can easily be converted from Rust slices and into Rust Vecs.

Many Qt types in cxx-qt-lib now have common Rust standard library traits implemented, such as Default, Display, Debug, PartialEq, Eq, PartialOrd, Ord, and arithmetic operators (Add, Sub, Mul, Div traits).

Cargo features have been added to cxx-qt-lib for easily converting between Qt types and types from widely used Rust crates:

  • QColor ⇄ rgb crate
  • QUrl ⇄ http and url crates
  • QByteArray ⇄ bytes crate
  • QDate, QDateTime, and QTime ⇄ chrono and time crates

The code generator now supports pointer types such as *mut T to refer to another Rust QObject in properties, signals, and invokables.

Build System

The build system can now register files from the Qt Resource System at build time, so calling a C++ function to initialize them is no longer needed. Likewise, QML types can now be registered at build time, instead of calling qmlRegisterType from C++ code. Together with new bindings for QGuiApplication and QQmlApplicationEngine, there is no longer a need for boilerplate C++ code to launch a QML application from Rust. The API is still evolving and we hope to improve this area in the next release to support the new ahead-of-time QML compilation tools.

For More Information

Find CXX-Qt on our GitHub repository and view our CHANGELOG for more details about the changes.

We also have a book with a getting started guide.

Discussions and contributions are welcome on GitHub!

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post CXX-Qt 0.5 Released appeared first on KDAB.

Working With Classes in Python and PyQt

Python supports object-oriented programming (OOP) through classes, which allow you to bundle data and behavior in a single entity. Python classes allow you to quickly model concepts by creating representations of real objects that you can then use to organize your code.

Most of the currently available GUI frameworks for Python developers, such as PyQt, PySide, and Tkinter, rely on classes to provide apps, windows, widgets, and more. This means that you'll be actively using classes for designing and developing your GUI apps.

In this tutorial, you'll learn how OOP and classes work in Python. This knowledge will allow you to quickly grasp how GUI frameworks are internally organized, how they work, and how you can use their classes and APIs to create robust GUI applications.

Defining Classes in Python

Python classes are templates or blueprints that allow us to create objects through instantiation. These objects will contain data representing the object's state, and methods that will act on the data providing the object's behavior.

Instantiation is the process of creating instances of a class by calling the class constructor with appropriate arguments.

Attributes and methods make up what is known as the class interface or API. This interface allows us to operate on the objects without needing to understand their internal implementation and structure.

Alright, it is time to start creating our own classes. We'll start by defining a Color class with minimal functionality. To do that in Python, you'll use the class keyword followed by the class name. Then you provide the class body in the next indentation level:

python
>>> class Color:
...     pass
...

>>> red = Color()

>>> type(red)
<class '__main__.Color'>

In this example, we defined our Color class using the class keyword. This class is empty. It doesn't have attributes or methods. Its body only contains a pass statement, which is Python's way to do nothing.

Even though the class is minimal, it allows us to create instances by calling its constructor, Colo(). So, red is an instance of Color. Now let's make our Color class more fun by adding some attributes.

Adding Class and Instance Аttributes

Python classes allow you to add two types of attributes. You can have class and instance attributes. A class attribute belongs to its containing class. Its data is common to the class and all its instances. To access a class attribute, we can use either the class or any of its instances.

Let's now add a class attribute to our Color class. For example, let's say we need to keep note of how many instance of Color your code creates. Then you can have a color_count attribute:

python
>>> class Color:
...     color_count = 0
...     def __init__(self):
...         Color.color_count += 1
...

>>> red = Color()
>>> green = Color()

>>> Color.color_count
2
>>> red.color_count
2

Now Color has a class attribute called color_count that gets incremented every time we create a new instance. We can quickly access that attribute using either the class directly or one of its instances, like red.

To follow up with this example, say that we want to represent our Color objects using red, green, and blue attributes as part of the RGB color model. These attributes should have specific values for specific instances of the class. So, they should be instance attributes.

To add an instance attribute to a Python class, you must use the .__init__() special method, which we introduced in the previous code but didn't explain. This method works as the instance initializer because it allows you to provide initial values for instance attributes:

python
>>> class Color:
...     color_count = 0
...     def __init__(self, red, green, blue):
...         Color.color_count += 1
...         self.red = red
...         self.green = green
...         self.blue = blue
...

>>> red = Color(255, 0, 0)

>>> red.red
255
>>> red.green
0
>>> red.blue
0

>>> Color.red
Traceback (most recent call last):
    ...
AttributeError: type object 'Color' has no attribute 'red'

Cool! Now our Color class looks more useful. It has the usual class attributes and also three new instance attributes. Note that, unlike class attributes, instance attributes can't be accessed through the class itself. They're specific to a concrete instance.

There's something that jumps into sight in this new version of Color. What is the self argument in the definition of .__init__()? This attribute holds a reference to the current instance. Using the name self to identify the current instance is a strong convention in Python.

We'll use self as the first or even the only argument to instance methods like .__init__(). Inside an instance method, we'll use self to access other methods and attributes defined in the class. To do that, we must prepend self to the name of the target attribute or method instance of the class.

For example, our class has an attribute .red that we can access using the syntax self.red inside the class. This will return the number stored under that name. From outside the class, you need to use a concrete instance instead of self.

Providing Behavior With Methods

A class bundles data (attributes) and behavior (methods) together in an object. You'll use the data to set the object's state and the methods to operate on that data or state.

Methods are just functions that we define inside a class. Like functions, methods can take arguments, return values, and perform different computations on an object's attributes. They allow us to make our objects usable.

In Python, we can define three types of methods in our classes:

  1. Instance methods, which need the instance (self) as their first argument
  2. Class methods, which take the class (cls) as their first argument
  3. Static methods, which take neither the class nor the instance

Let's now talk about instance methods. Say that we need to get the attributes of our Color class as a tuple of numbers. In this case, we can add an .as_tuple() method like the following:

python
class Color:
    representation = "RGB"

    def __init__(self, red, green, blue):
        self.red = red
        self.green = green
        self.blue = blue

    def as_tuple(self):
        return self.red, self.green, self.blue

This new method is pretty straightforward. Since it's an instance method, it takes self as its first argument. Then it returns a tuple containing the attributes .red, .green, and .blue. Note how you need to use self to access the attributes of the current instance inside the class.

This method may be useful if you need to iterate over the RGB components of your color objects:

python
>>> red = Color(255, 0, 0)
>>> red.as_tuple()
(255, 0, 0)

>>> for level in red.as_tuple():
...     print(level)
...
255
0
0

Our as_tuple() method works great! It returns a tuple containing the RGB components of our color objects.

We can also add class methods to our Python classes. To do this, we need to use the @classmethod decorator as follows:

python
class Color:
    representation = "RGB"

    def __init__(self, red, green, blue):
        self.red = red
        self.green = green
        self.blue = blue

    def as_tuple(self):
        return self.red, self.green, self.blue

    @classmethod
    def from_tuple(cls, rbg):
        return cls(*rbg)

The from_tuple() method takes a tuple object containing the RGB components of a desired color as an argument, creates a valid color object from it, and returns the object back to the caller:

python
>>> blue = Color.from_tuple((0, 0, 255))
>>> blue.as_tuple()
(0, 0, 255)

In this example, we use the Color class to access the class method from_tuple(). We can also access the method using a concrete instance of this class. However, in both cases, we'll get a completely new object.

Finally, Python classes can also have static methods that we can define with the @staticmethod decorator:

python
class Color:
    representation = "RGB"

    def __init__(self, red, green, blue):
        self.red = red
        self.green = green
        self.blue = blue

    def as_tuple(self):
        return self.red, self.green, self.blue

    @classmethod
    def from_tuple(cls, rbg):
        return cls(*rbg)

    @staticmethod
    def color_says(message):
        print(message)

Static methods don't operate either on the current instance self or the current class cls. These methods can work as independent functions. However, we typically put them inside a class when they are related to the class, and we need to have them accessible from the class and its instances.

Here's how the method works:

python
>>> Color.color_says("Hello from the Color class!")
Hello from the Color class!

>>> red = Color(255, 0, 0)
>>> red.color_says("Hello from the red instance!")
Hello from the red instance!

This method accepts a message and prints it on your screen. It works independently from the class or instance attributes. Note that you can call the method using the class or any of its instances.

Writing Getter & Setter Methods

Programming languages like Java and C++ rely heavily on setter and getter methods to retrieve and update the attributes of a class and its instances. These methods encapsulate an attribute allowing us to get and change its value without directly accessing the attribute itself.

For example, say that we have a Label class with a text attribute. We can make text a non-public attribute and provide getter and setter methods to manipulate the attributes according to our needs:

python
class Label:
    def __init__(self, text):
        self.set_text(text)

    def text(self):
        return self._text

    def set_text(self, value):
        self._text = str(value)

In this class, the text() method is the getter associated with the ._text attribute, while the set_text() method is the setter for ._text. Note how ._text is a non-public attribute. We know this because it has a leading underscore on its name.

The setter method calls str() to convert any input value into a string. Therefore, we can call this method with any type of object. It will convert any input argument into a string, as you will see in a moment.

If you come from programming languages like Java or C++, you need to know Python doesn't have the notion of private, protected, and public attributes. In Python, you'll use a naming convention to signal that an attribute is non-public. This convention consists of adding a leading underscore to the attribute's name. Note that this naming pattern only indicates that the attribute isn't intended to be used directly. It doesn't prevent direct access, though.

This class works as follows:

python
>>> label = Label("Python!")

>>> label.text()
'Python!'

>>> label.set_text("PyQt!")
>>> label.text()
'PyQt!'

>>> label.set_text(123)
>>> label.text()
'123'

In this example, we create an instance of Label. The original text is passed to the class constructor, Label(), which automatically calls __init__() to set the value of ._text by calling the setter method text(). You can use text() to access the label's text and set_text() to update it. Remember that any input will be converted into a string, as we can see in the final example above.

Note that the Label class above is just a toy example, don't confuse this class with similarly named classes from GUI frameworks like PyQt, PySide, and Tkinter.

The getter and setter pattern is pretty common in languages like Java and C++. Because PyQt and PySide are Python bindings to the Qt library, which is written in C++, you'll be using this pattern a lot in your Qt-based GUI apps. However, this pattern is less popular among Python developers. Instead, they use the @property decorator to hide attributes behind properties.

Here's how most Python developer will write their Label class:

python
class Label:
    def __init__(self, text):
        self.text = text

    @property
    def text(self):
        return self._text

    @text.setter
    def text(self, value):
        self._text = str(value)

This class defines .text as a property. This property has getter and setter methods. Python calls them automatically when we access the attribute or update its value in an assignment:

python
>>> label = Label("Python!")

>>> label.text
'Python!'

>>> label.text = "PyQt"
>>> label.text
'PyQt'

>>> label.text = 123
>>> label.text
'123'

Python properties allow you to add function behavior to your attributes while permitting you to use them as normal attributes instead of as methods.

Writing Special Methods

Python supports many special methods, also known as dunder or magic methods, that are part of its class mechanism. We can identify these methods because their names start and end with a double underscore, which is the origin of their other name: dunder methods.

These methods accomplish different tasks in Python's class mechanism. They all have a common feature: Python calls them automatically depending on the operation we run.

For example, all Python objects are printable. We can print them to the screen using the print() function. Calling print() internally falls back to calling the target object's __str__() special method:

python
>>> label = Label("Python!")

>>> print(label)
<__main__.Label object at 0x10354efd0>

In this example, we've printed our label object. This action provides some information about the object and the memory address where it lives. However, the actual output is not very useful from the user's perspective.

Fortunately, we can improve this by providing our Label class with an appropriate __str__() method:

python
class Label:
    def __init__(self, text):
        self.text = text

    @property
    def text(self):
        return self._text

    @text.setter
    def text(self, value):
        self._text = str(value)

    def __str__(self):
        return self.text

The __str__() method must return a user-friendly string representation for our objects. In this case, when we print an instance of Label to the screen, the label's text will be displayed:

python
>>> label = Label("Python!")

>>> print(label)
Python!

As you can see, Python takes care of calling __str__() automatically when we use the print() function to display our instances of Label.

Another special method that belongs to Python's class mechanism is __repr__(). This method returns a developer-friendly string representation of a given object. Here, developer-friendly implies that the representation should allow a developer to recreate the object itself.

python
class Label:
    def __init__(self, text):
        self.text = text

    @property
    def text(self):
        return self._text

    @text.setter
    def text(self, value):
        self._text = str(value)

    def __str__(self):
        return self.text

    def __repr__(self):
        return f"{type(self).__name__}(text='{self.text}')"

The __repr__() method returns a string representation of the current objects. This string differs from what __str__() returns:

python
>>> label = Label("Python!")
>>> label
Label(text='Python!')

Now when you access the instance on your REPL session, you get a string representation of the current object. You can copy and paste this representation to recreate the object in an appropriate environment.

Reusing Code With Inheritance

Inheritance is an advanced topic in object-oriented programming. It allows you to create hierarchies of classes where each subclass inherits all the attributes and behaviors from its parent class or classes. Arguably, code reuse is the primary use case of inheritance.

Yes, we code a base class with a given functionality and make that functionality available to its subclass through inheritance. This way, we implement the functionality only once and reuse it in every subclass.

Python classes support single and multiple inheritance. For example, let's say we need to create a button class. This class needs .width and .height attributes that define its rectangular shape. The class also needs a label for displaying some informative text.

We can code this class from scratch, or we can use inheritance and reuse the code of our current Label class. Here's how to do this:

python
class Button(Label):
    def __init__(self, text, width, height):
        super().__init__(text)
        self.width = width
        self.height = height

    def __repr__(self):
        return (
            f"{type(self).__name__}"
            f"(text='{self.text}', "
            f"width={self.width}, "
            f"height={self.height})"
        )

To inherit from a parent class in Python, we need to list the parent class or classes in the subclass definition. To do this, we use a pair of parentheses and a comma-separated list of parent classes. If we use several parent classes, then we're using multiple inheritance, which can be challenging to reason about.

The first line in __init__() calls the __init__() method on the parent class to properly initialize its .text attribute. To do this, we use the built-in super() function. Then we define the .width and .height attributes, which are specific to our Button class. Finally, we provide a custom implementation of __repr__().

Here's how our Button class works:

python
>>> button = Button("Ok", 10, 5)

>>> button.text
'Ok'
>>> button.text = "Click Me!"
>>> button.text
'Click Me!'

>>> button.width
10
>>> button.height
5

>>> button
Button(text='Ok', width=10, height=5)
>>> print(button)
Click Me!

As you can conclude from this code, Button has inherited the .text attribute from Label. This attribute is completely functional. Our class has also inherited the __str__() method from Label. That's why we get the button's text when we print the instance.

Using Classes in PyQt GUI Apps

Everything we've learned so far about Python classes is the basis of our future work in GUI development. When it comes to working with PyQt, PySide, Tkinter, or any other GUI framework, we'll heavily rely on our knowledge of classes and OOP because most of them are based on classes and class hierarchies.

We'll now look at how to use inheritance to create some GUI-related classes. For example, when we create an application with PyQt or PySide, we usually have a main window. To create this window, we typically inherit from QMainWindow:

python
from PyQt6.QtWidgets import QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

python
from PySide6.QtWidgets import QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

In the definition of our Window class, we use the QMainWindow class as the parent class. This tells Python that we want to define a class that inherits all the functionalities that QMainWindow provides.

We can continue adding attributes and methods to our Window class. Some of these attributes can be GUI widgets, such as labels, buttons, comboboxes, checkboxes, line edits, and many others. In PyQt, we can create all these GUI components using classes such as QLabel, QPushButton, QComboBox, QCheckBox, and QLineEdit.

All of them have their own sets of attributes and methods that we can use according to our specific needs when designing the GUI of a given application.

As we've seen, Python allows us to write classes that work as templates that you can use to create concrete objects that bundle together data and behavior. The building blocks of Python classes are:

  • Attributes, which hold the data in a class
  • Methods, which provide the behaviors of a class

The attributes of a class define the class's data, while the methods provide the class's behaviors, which typically act on that data.

To better understand OOP and classes in Python, we should first discuss some terms that are commonly used in this aspect of Python development:

  • Classes are blueprints or templates for creating objects -- just like a blueprint for creating a car, plane, house, or anything else. In programming, this blueprint will define the data (attributes) and behavior (methods) of the object and will allow us to create multiple objects of the same kind.

  • Objects or Instances are the realizations of a class. We can create objects from the blueprint provided by the class. For example, you can create John's car from a Car class.

  • Methods are functions defined within a class. They provide the behavior of an object of that class. For example, our Car class can have methods to start the engine, turn right and left, stop, and so on.

  • Attributes are properties of an object or class. We can think of attributes as variables defined in a class or object. Therefore, we can have:

    • class attributes, which are specific to a concrete class and common to all the instances of that class. You can access them either through the class or an object of that class. For example, if we're dealing with a single car manufacturer, then our Car class can have a manufacturer attribute that identifies it.
    • instance attributes, which are specific to a concrete instance. You can access them through the specific instance. For example, our Car class can have attributes to store properties such as the maximum speed, the number of passengers, the car's weight, and so on.
  • Instantiation is the process of creating an individual instance from a class. For example, we can create John's car, Jane's car, and Linda's car from our Car class through instantiation. In Python, this process runs through two steps:

    1. Instance creation: Creates a new object and allocates memory for storing it.
    2. Instance initialization: Initializes all the attributes of the current object with appropriate values.
  • Inheritance is a mechanism of code reuse that allows us to inherit attributes and methods from one or multiple existing classes. In this context, we'll hear terms like:

    • Parent class: The class we're inheriting from. This class is also known as the superclass or base class. If we have one parent class, then we're using single inheritance. If we have more than one parent class, then we're using multiple inheritance.
    • Child class: The class that inherits from a given parent. This class is also known as the subclass.

Don't feel frustrated or bad if you don't understand all these terms immediately. They'll become more familiar with use as you use them in your own Python code. Many of our GUI tutorials make use of some or all of these concepts.

Conclusion

Now you know the basics of Python classes. You also learned fundamental concepts of object-oriented programming, such as inheritance. You also learned that most GUI frameworks are heavily based on classes. Therefore knowing about classes will open the door to begin building your own GUI app using PyQt, PySide, Tkinter, or any other GUI framework for Python.

For more, see the complete PyQt6 tutorial.