What is in Qt Insight v1.0?

Qt Insight is a product analytics solution providing real-world data into how people use a software application or a digital device. The first version of Qt Insight for General Availability was released in March 2023. Qt Insight consists of three major components: The Qt Tracker software library embedded in the Qt application, the Qt Insight Cloud Services, and the Qt Insight Console for analyzing behavioral data in a web browser.

Qt for MCUs 2.2.4 LTS Released

Qt for MCUs 2.2.4 LTS (Long-Term Support) has been released and is available for download. As a patch release, Qt for MCUs 2.2.4 LTS provides bug fixes and other improvements, and maintains source compatibility with Qt for MCUs 2.2.x. It does not add any new functionality.

Shaping the Future of Digital Experience - UI Framework Performance

After discussing the UI framework's foundational elements, namely the set of ready-made solutions enabling rapid cross-platform development and efficient time-to-market of UI apps, we look here at what is needed to achieve the kind of smooth, seamless performance that defines the most engaging applications on the market. But let's start with a clarification of what is a "UI app" and how it differs from other types of visual software, like video games or design authoring tools.

Qt Digital Advertising in Toradex's Torizon

Monetizing your embedded devices can seem like a daunting challenge at first. With the large variety of ad networks and technologies, plus the uncertainty of support for various hardware platforms, it might not seem worth the effort. However, serving ads on embedded devices running Qt has never been easier with Qt Digital Advertising and Toradex's Torizon.

With just a few simple docker commands, you'll learn how to build and run a demo that serves ads from Qt's Digital Advertising plugin quickly and easily using TorizonCore. While digital ads are currently supported on every TQC-supported target and verified QBSP target*, the boards tested with this demo are the Colibri iMX7 and the Apalis iMX8, both by Toradex.

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.

Qt 6.4.3 Released

We have released Qt 6.4.3 today. As a patch release, Qt 6.4.3 does not introduce any new features but contains more than 300 bug fixes, security updates, and other improvements to the top of the Qt 6.4.2 release. See more information about the most important changes and bug fixes from Qt 6.4.3 release note.

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 more, see the complete PySide6 tutorial.

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 containers 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 larger 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.

Qt 6 Debugging in Visual Studio and VS Code

Not all Qt developers are using Qt Creator as their main IDE for development. Some of us are using other IDEs, like Visual Studio, Visual Studio Code or CLion, per choice or because a project requires it.

On Windows, with the MSVC compiler, the debugging experience with Qt 6 can be quite frustrating for some Qt types, like QMap, QHash or QVariant. See, for example, the debug variable view for the same project running with Qt Creator (left) and Visual Studio Code (right):

I don’t know about you, but for me the VS Code view is gibberish. It was way better with Qt 5, as seen below:

Let’s see how to improve things…

Use the Natvis Framework

The Natvis framework allows developers to write custom schemas to help visualize native objects. This is already used by the Qt VS Tools for Visual Studio, with Qt 5 and a Qt 6 specific files. You can download the official Qt natvis files here: Qt VS Tool github.

They are pre-processed and copied automatically by the plugin to %HOME%\Documents\Visual Studio 2022\Visualizers. If you want to use them directly from github, make sure to remove all occurrences of ##NAMESPACE##::.

If you look closely at the size of the file, you can see that the Qt 5 Natvis file is 40.9 KB. The Qt 6 Natvis file is only 18.5 KB, which is not 50% smaller than the Qt 5 — au contraire! This means that a lot of types are not working anymore, most likely because the Qt 5 implementation didn’t work during the migration to Qt 6 due to changes in the underlying data.

Let’s see how far we can improve things here. This blog won’t be about the how (it involved a lot of cursing…), but only about the improvements made to the qt6.natvis file.

Changes

For developers in a hurry, you can download the updated Natvis file here:

https://github.com/KDAB/KDToolBox/blob/master/qt/qt6_natvis/qt6.natvis

QMap Improvements

QMap before       QMap after

QMap was already working, but I wanted a nicer view, with [key]: value display.

QHash Improvements

QHash before       QHash after

QHash was the very reason I started the journey; I just wanted to see something, ideally, like QMap. Unfortunately, it’s not supported on CLion, which is missing support for the CustomListItems Natvis tag.

QMultiHash Improvements

QMultiHash (with QHash changes)       QMultiHash after

Once QHash was done, QMultiHash was quite easy. Please note that the view is slightly different from the std::unordered_map equivalent; the Qt version is showing a list of values for one key, while the std version shows a list of (key, value) pairs with duplicated keys. Like QHash, QMultiHash view is not supported in CLion.

QVariant Improvements

Static primitives       Core templates       Core classes

After looking at the containers, I decided to look at QVariant. Unfortunately, not all types are properly handled, but at least the static primitives, core templates, and most of the core classes are. If the type is unknown, it will show at least the typeId, so the developer can know what is inside the QVariant. The full list is available here: Qt6 Natvis documentation

How to Use this New qt6.natvis File

Visual Studio

Follow the isntructions here to add a natvis file to your project: Add a .natvis file to a C++ project.

VS Code

We already discussed it in another blog post: VS Code for Qt Applications – Part 2. Just a quick recap:

  • create a launch.json file (that’s the “Debug: Add Configuration…” command)
  • Set the visualizerFile property to the qt6.natvis file

This is an example launch.json file that works well with CMake project:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "qt6.4.2-msvc",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${command:cmake.launchTargetPath}",
            "visualizerFile": "${workspaceFolder}/qt6.natvis",
            "sourceFileMap": {
                "C:/work/build/qt5_workdir/w/s": "C:/Qt/6.4.2/Src",
                "Q:/qt5_workdir/w/s": "C:/Qt/6.4.2/Src",
                "C:/Users/qt/work/install": "C:/Qt/6.4.2/Src",
                "C:/Users/qt/work/qt": "C:/Qt/6.4.2/Src"
            }
        }
    ]
}

Why Not Do the Same As Qt Creator?

That’s a good question, and my first attempt at trying to fix this issue was to look at Qt Creator. After all, it’s using the same compiler and debugger. Surely, I can find something I can use. Quoting the documentation:

Qt Creator extends the command line debugger by loading the qtcreatorcdbext.dll extension library into it. The library must be available in the libs\qtcreatorcdbext64 and libs\qtcreatorcdbext32 folder. To install it there, select Qt Creator CDB Debugger Support when you install Qt Creator.

Again, quoting the documentation:

Qt Creator uses Python scripts to translate raw memory contents and type information data from native debugger backends (GDB, LLDB, and CDB are currently supported) into the form presented to the user in the Locals and Expressions views.

That’s the reason why the debugger is slower in Qt Creator (you can disable it from the Debugger->CDB preference page, with the “Use Python dumper” option). Unfortunately, it’s not usable for Visual Studio and VS Code.

Conclusion

It’s unfortunately impossible to go as far as what Qt Creator can do, but the changes to the qt6.natvis file improve the debugging experience quite a lot already, and we are close to feature parity with qt5.natvis. The file is part of our KDToolBox project. KDToolBox is a collection of useful code, scripts and snippets you are free to use; you can download it here:

https://github.com/KDAB/KDToolBox/blob/master/qt/qt6_natvis/qt6.natvis

This file can be used for Visual Studio, VS Code, and CLion IDEs, with the exception of QHash and QMutiHash not working with CLion. If something is missing, send me a note, no promises though.

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 Qt 6 Debugging in Visual Studio and VS Code 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 &Acyttributes

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.

Create Beautiful Cross-Platform Apps with Felgo 4.0 and Qt 6.4.1

It was a long journey but it is finally here: Felgo 4.0! 🎉

Since Qt 6 was first released, the list of supported modules and features has grown steadily, and each new version brought many new additions and fixes. During this time we spent a lot of effort to port the Felgo components and thoroughly test all our main use-cases on desktop, mobile and embedded platforms. As Qt 6 incorporates many changes, we wanted to make sure the transition is as smooth as possible so we can support all the things you know and love from Felgo 3.

Felgo 4.0 makes it possible to develop Felgo apps with all the latest features of Qt 6.4.1. The new release brings support for the CMake build system and updates all Felgo controls to use Qt Quick Controls 2. It also includes a custom build of the Qt Location module because Qt 6.4.1 does not support it yet. This means that you can already use Felgo 4 for creating applications that include map features.

To get Felgo 4, download the latest installer from the website. This post summarises all you need to know for updating your app to Felgo 4 and Qt 6.4.1:

QObjects, Ownership, propagate_const and C++ Evolution

A very common implementation pattern for QObject subclasses is to declare its child QObjects as data members of type “pointer to child.” Raise your hand No, keep your hand on your computer input device 🙂 Nod if you have ever seen code like this (and maybe even written code like this yourself):

class MyWidget : public QWidget {
    Q_OBJECT
public:
    explicit MyWidget(QWidget *parent = nullptr);

private:
    DataModel *m_dataModel;
    QTreeView *m_view;
    QLineEdit *m_searchField;
};

A fairly common question regarding this pattern is: “Why are we using (raw) pointers for data members”?

Of course, if we are just accessing an object that we don’t own/manage, it makes perfect sense to simply store a pointer to it (so that we’re actually able to use the object). A raw pointer is actually the Modern C++™ design here, to deliberately express the lack of ownership.

Ownership Models

The answer becomes slightly more nuanced when MyWidget actually owns the objects in question. In this case, the implementation designs are basically two:

  1. As shown above, use pointers to the owned objects. Qt code would typically still use raw pointers in this case, and rely on the parent/child relationship in order to manage the ownership. In other words, we will use the pointers to access the objects but not to manage their lifetime.
    class MyWidget : public QWidget {
        Q_OBJECT
    public:
        explicit MyWidget(QWidget *parent = nullptr);
    
    private:
        DataModel *m_dataModel;
        QTreeView *m_view;
        QLineEdit *m_searchField;
    };
    
    MyWidget::MyWidget(QWidget *parent)
      : QWidget(parent)
    {
        // Children parented to `this`
        m_dataModel = new DataModel(this);   
        m_view = new QTreeView(this);          
        m_searchField = new QLineEdit(this); 
    }
    

    This approach makes developers familiar with Modern C++ paradigms slightly uncomfortable: a bunch of “raw news” in the code, no visible deletes — eww! Sure, you can replace the raw pointers with smart pointers, if you wish to. But that’s a discussion for another blog post.

  2. Another approach is to declare the owned objects as…objects, and not pointers:
    class MyWidget : public QWidget {
        Q_OBJECT
    public:
        explicit MyWidget(QWidget *parent = nullptr);
    
    private:
        DataModel m_dataModel; // not pointers
        QTreeView m_view;
        QLineEdit m_searchField;
    };
    
    MyWidget::MyWidget(QWidget *parent)
      : QWidget(parent)
    {
    }
    

    This makes it completely clear that the lifetime of those objects is tied to the lifetime of MyWidget.

To Point, or Not to Point?

“So what is the difference between the two approaches?”, you may be wondering. That is just another way to ask: “which one is better and should I use in my code?”

The answers to this question involve a lot of interesting aspects, such as:

  • Using pointers will significantly increase the number of memory allocations: we are going to do one memory allocation per child object. The irony here is that, most of the time, those child objects are, themselves, pimpl’d. Creating a QLineEdit object will, on its own, already allocate memory for its private data; we’re compounding that allocation with another one. Eventually, in m_searchField, we’ll store a pointer to a heap-allocated…”pointer” (the QLineEdit object, itself, which just contains the pimpl pointer) that points to another heap-allocate private object (QLineEditPrivate). Yikes!
  • Pointers allow the user to forward declare the pointed-to datatypes in MyWidget‘s header. This means that users of MyWidget do not necessarily have to include the headers that define QTreeView, QlineEdit, etc. This improves compilation times.
  • Pointers allow you to establish “grandchildren” and similar, not just direct children. A grandchild is going to be deleted by someone else (its parent), and not directly by our MyWidget instances. If that grandchild is a sub-object of MyWidget (like in the second design), this will mean destroying a sub-object via delete, and that’s bad.
  • Pointers force (or, at least, should force) users to properly parent the allocated objects. This has an impact in a few cases, for instance if one moves the parent across threads. When using full objects as data members, it’s important to remember to establish a parent/child relationship by parenting them.
    class MyObject : public QObject {
        Q_OBJECT
        QTimer m_timer;              // timer as sub-object
    public:
        MyObject(QObject *parent)
          : QObject(parent)
          , m_timer(this)            // remember to do this...!
        {}
    };
    
    MyObject *obj = new MyObject;
    obj->moveToThread(anotherThread); // ...or this will likely break
    

    Here the parent/child relationship is not going to be used to manage memory, but only to keep the objects together when moving them between threads (so that the entire subtree is moved by moveToThread).

So, generally speaking, using pointers seems to offer more advantages than disadvantages, and that is why they are so widely employed by developers that use Qt.

Const Correctness

All this is good and everything — and I’ve heard it countless times. What does all of this have to do with the title of the post? We’re getting there!

A consideration that I almost never hear as an answer to the pointer/sub-object debate is const correctness.

Consider this example:

class MyWidget : public QWidget {
    Q_OBJECT
    QLineEdit m_searchField; // sub-object
public:
    explicit MyWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
    }

    void update() const {
        m_searchField.setText("Search"); // ERROR
    }
};

This does not compile; update() is a const method and, inside of it, m_searchField is a const QLineEdit. This means we cannot call setText() (a non-const method) on it.

From a design perspective, this makes perfect sense; in a const method we are not supposed to modify the “visible state” of *this and the “visible state” of QLineEdit logically belongs to the state of the *this object, as it’s a sub-object.


However, we have just discussed that the design of using sub-objects isn’t common; Qt developers usually employ pointers. Let’s rewrite the example:

class MyWidget : public QWidget {
    Q_OBJECT
    QLineEdit *m_searchField; // pointer
public:
    explicit MyWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        m_searchField = new QLineEdit(this);
    }

    void update() const {
        m_searchField->setText("Search");
    }
};

What do you think happens here? Is the code still broken?

No, this code compiles just fine. We’re modifying the contents of m_searchField from within a const method.

Pointer-to-const and Const Pointers

This is not entirely surprising to seasoned C++ developers. In C++ pointers and references are shallow const; a const pointer can point to a non-const object and allow the user to mutate it.

Constness of the pointer and constness of the pointed-to object are two independent qualities:


// Non-const pointer, pointing to non-const object
QLineEdit *p1 = ~~~;                
  p1 = new QLineEdit;               // OK, can mutate the pointer
  p1->mutate();                     // OK, can mutate the pointed-to object

// Const pointer, pointing to non-const object
QLineEdit *const p2 = ~~~;          
  p2 = new QLineEdit;               // ERROR, cannot mutate the pointer
  p2->mutate();                     // OK, can mutate the pointed-to

// Non-const pointer, pointing to const object
const QLineEdit *p3 = ~~~;          
  p3 = new QLineEdit;               // OK, can mutate the pointer
  p3->mutate();                     // ERROR, cannot mutate the pointed-to object

// Non-const pointer, just like p3, but using East Const (Qt uses West Const)
QLineEdit const *p3b = ~~~;         

// Const pointer, pointing to const object
const QLineEdit * const p4 = ~~~ ;  
  p4 = new QLineEdit;               // ERROR, cannot mutate the pointer
  p4->mutate();                     // ERROR, cannot mutate the pointed-to object

// Const pointer, just like p4, using East Const
QLineEdit const * const p4b = ~~~;  

This is precisely what is happening in our update() method. In there, *this is const, which means that m_searchField is a const pointer. (In code, this type would be expressed as QLineEdit * const.)

I’ve always felt mildly annoyed by this situation and have had my share of bugs due to modifications of subobjects from const methods. Sure, most of the time, it was entirely my fault, calling the wrong function on the subobject in the first place. But some of the time, this has made me have “accidental” const functions with visible side-effects (like the update() function above)!

Deep-const Propagation

The bad news is that there isn’t a solution for this issue inside the C++ language. The good news is that there is a solution in the “C++ Extensions for Library Fundamentals”, a set of (experimental) extensions to the C++ Standard Library. This solution is called std::experimental::propagate_const (cppreference, latest proposal at the time of this writing).

propagate_const acts as a pointer wrapper (wrapping both raw pointers and smart pointers) and will deeply propagate constness. If a propagate_const object is const itself, then the pointed-to object will be const.

// Non-const wrapper => non-const pointed-to object
propagate_const<QLineEdit *> p1 = ~~~;
  p1->mutate();  // OK

// Const wrapper => const pointed-to object
const propagate_const<QLineEdit *> p2 = ~~~;
  p2->mutate();  // ERROR

// Const reference to the wrapper => const pointed-to object
const propagate_const<QLineEdit *> & const_reference = p1;
  const_reference->mutate(); // ERROR

This is great, because it means that we can use propagate_const as a data member instead of a raw pointer and ensure that we can’t accidentally mutate a child object:

class MyWidget : public QWidget {
    Q_OBJECT
    std::experimental::propagate_const<QLineEdit *> m_searchField;  // drop-in replacement

public:
    explicit MyWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        m_searchField = new QLineEdit(this);
    }

    void update() const {
        m_searchField->clear(); // ERROR!
    }
};

Again, if you’re a seasoned C++ developer, this shouldn’t come as a surprise to you. My colleage Marc Mutz wrote about this a long time ago, although in the context of the pimpl pattern. KDAB’s RND efforts in this area led to things such as a deep-const alternative to QExplicitlySharedDataPointer (aptly named QExplicitlySharedDataPointerV2) and QIntrusiveSharedPointer.

propagate_const and Child QObjects

As far as I know, there hasn’t been much research about using propagate_const at large in a Qt-based project in order to hold child objects. Recently, I’ve had the chance to use it in a medium-sized codebase and I want to share my findings with you.

Compiler Support

Compiler support for propagate_const is still a problem, notably because MSVC does not implement the Library Fundamentals TS. It is, however, shipped by GCC and Clang and there are third party implementations available, including one in KDToolBox (keep reading!).

Source Compatibility

If one already has an existing codebase, one may want to start gradually adopting propagate_const by replacing existing usages of raw pointers. Unfortunately, in a lot of cases, propagate_const isn’t simply a drop-in replacement and will cause a number of source breaks.

What I’ve discovered (at the expense of my own sanity) is that some of these incompatibilities are caused by accidentally using niche C++ features; in order to be compatible with older C++ versions, implementations accidentally introduce quirks, some by compiler bugs.

Here’s all the nitty gritty details; feel free to skim over them 🙂

Broken Conversions to Superclasses

Consider this example:

class MyWidget : public QWidget {
    Q_OBJECT
    QLineEdit *m_searchField;

public:
    explicit MyWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        // ... set up layouts, etc. ...
        m_searchField = new QLineEdit(this);
        layout()->addWidget(m_searchField); // this is addWidget(QWidget *)
    }
};

Today, this works just fine. However, this does not compile:

class MyWidget : public QWidget {
    Q_OBJECT
    // change to propagate_const ...
    std::experimental::propagate_const<QLineEdit *> m_searchField;  

public:
    explicit MyWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        // ... set up layouts, etc. ...
        m_searchField = new QLineEdit(this);
        layout()->addWidget(m_searchField);
        // ^^^ ERROR, cannot convert propagate_const to QWidget *
    }
};

C++ developers know the drill: smart pointer classes are normally not a 1:1 replacement of raw pointers. Most smart pointer classes do not implicitly convert to raw pointers, for very good reasons. In situations where raw pointers are expected (for instance, addWidget in the snippet above wants a parameter of type QWidget *), one has to be slightly more verbose, for instance, by calling m_searchField.get().

Here, I was very confused. propagate_const was meant to be a 1:1 replacement. Here are a couple of quotes from the C++ proposal for propagate_const:

The change required to introduce const-propagation to a class is simple and local enough to be enforced during code review and taught to C++ developers in the same way as smart-pointers are taught to ensure exception safety.

operator value*

When T is an object pointer type operator value* exists and allows implicit conversion to a pointer. This avoids using get to access the pointer in contexts where it was unnecesary before addition of the propagate_const wrapper.

In other words, it has always been a design choice to keep source compatibility in cases like the one above! propagate_const<T *> actually has a conversion operator to T *.

So why doesn’t the code above work? Here’s a reduced testcase:

std::experimental::propagate_const<Derived *> ptr;

Derived *d1 = ptr;  // Convert precisely to Derived *: OK
Base *b1    = ptr;  // Convert to a pointer to a base: ERROR

Base *b2    = static_cast<Derived *>(ptr); // OK
Base *b3    = static_cast<Base *>(ptr);    // ERROR

Base *b4    = ptr.get();  // OK

At first, this almost caused me to ditch the entire effort; Qt uses inheritance very aggressively and we pass pointers to derived classes to functions taking pointers to base classes all the time. If that required sprinkling calls to .get() (or casts) everywhere, it would have been a massive refactoring. This is something that a tool like clazy can automate for you; but that’s a topic for another time.

Still, I couldn’t find a justification for the behavior shown above. Take a look at this testcase where I implement a skeleton of propagate_const, focusing on the conversion operators:

template <typename T>
class propagate_const
{
public:
    using element_type
        = std::remove_reference_t<decltype(*std::declval<T>())>;

    operator element_type *();
    operator const element_type *() const;
};

propagate_const<Derived *> ptr;
Base *b = ptr; // OK

This now compiles just fine. Let’s make it 100% compatible with the specification by adding the necessary constraints:

template <typename T>
class propagate_const
{
public:
    using element_type
        = std::remove_reference_t<decltype(*std::declval<T>())>;

    operator element_type *()
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<T, element_type *>);

    operator const element_type *() const
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<const T, const element_type *>);
};

propagate_const<Derived *> ptr;
Base *b = ptr;    // still OK

This still works flawlessly. So what’s different with propagate_const as shipped by GCC or Clang?

libstdc++ and libc++ do not use constraints (as in, the C++20 feature) on the conversion operators because they want to make propagate_const also work in earlier C++ versions. In fact, they use the pre-C++20 way — we have to make overloads available only when certain conditions are satisfied: SFINAE.

The conversion operators in libstdc++ and libc++ are implemented like this:

template <typename T>
class propagate_const
{
public:
    using element_type = std::remove_reference_t<decltype(*std::declval<T>())>;

    template <typename U = T, 
      std::enable_if_t<(std::is_pointer_v<U> 
                       || std::is_convertible_v<U, element_type *>),
        bool> = true>
    operator element_type *();

    template <typename U = T, 
      std::enable_if_t<(std::is_pointer_v<U> 
                       || std::is_convertible_v<const U, const element_type *>), 
        bool> = true>
    operator const element_type *() const;
};

propagate_const<Derived *> ptr;
Base *b = ptr;    // ERROR

This specific implementation is broken on all major compilers, which refuse to use the operators for conversions other than to precisely element_type * and nothing else.

What’s going on? The point is that converting propagate_const to Base * is a user-defined conversion sequence: first we convert propagate_const to a Derived * through the conversion operator. Then, perform a pointer conversion from Derived * to Base *.

But here’s what The Standard says when templates are involved:

If the user-defined conversion is specified by a specialization of a conversion function template, the second standard conversion sequence shall have exact match rank.

That is, we cannot “adjust” the return type of a conversion function template, even if the conversion would be implicit.

The takeaway is: SFINAE on conversion operators is user-hostile.

Restoring the Conversions Towards Superclasses

We can implement a workaround here by deviating a bit from the specification. If we add conversions towards any pointer type that T implicitly converts to, then GCC and Clang (and MSVC) are happy:

template <typename T>
class non_standard_propagate_const  // not the Standard one!
{
public:
    using element_type = std::remove_reference_t<decltype(*std::declval<T>())>;

    // Convert to "any" pointer type
    template <typename U, 
      std::enable_if_t<std::is_convertible_v<T, U *>, bool> = true>
    operator U *();

    template <typename U, 
      std::enable_if_t<std::is_convertible_v<const T, const U *>, bool> = true>
    operator const U *() const;
};

non_standard_propagate_const<Derived *> ptr;
Base *b = ptr;    // OK

This is by far the simplest solution; it is, however, non-standard.

I’ve implemented a better solution in KDToolBox by isolating the conversion operators in base classes and applying SFINAE on the base classes instead.

For instance, here’s the base class that defines the non-const conversion operator:

// Non-const conversion
template <typename T,
          bool = std::disjunction_v<
              std::is_pointer<T>,
              std::is_convertible<T, propagate_const_element_type<T> *>
              >
          >
struct propagate_const_non_const_conversion_operator_base
{
};

template <typename T>
struct propagate_const_non_const_conversion_operator_base<T, true>
{
    constexpr operator propagate_const_element_type<T> *();
};

Then, propagate_const<T> will inherit from propagate_const_non_const_conversion_operator_base<T> and the non-template operator will be conditionally defined.

Deletion and Pointer Arithmetic

Quoting again from the original proposal for propagate_const:

Pointer arithemtic [sic] is not supported, this is consistent with existing practice for standard library smart pointers.

… or is it? Herb Sutter warned us about having implicit conversions to pointers, as they would also enable pointer arithmetic.

Consider again the C++20 version:

template <typename T>
class propagate_const
{
public:
    using element_type
        = std::remove_reference_t<decltype(*std::declval<T>())>;

    operator element_type *()
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<T, element_type *>);

    operator const element_type *() const
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<const T, const element_type *>);
};

propagate_const<SomeClass *> ptr;
SomeClass *ptr2 = ptr + 1; // OK!

The arithmetic operators are not deleted for propagate_const. This means that the implicit conversion operators to raw pointers can (and will) be used, effectively enabling pointer arithmetic — something that the proposal said it did not want to support!

non_standard_propagate_const (as defined above), instead, does not support pointer arithmetic, as it needs to deduce the pointer type to convert to, and that deduction is not possible.


On a similar note, what should delete ptr; do, if ptr is a propagate_const object? Yes, there is some Qt-based code that simply deletes objects in an explicit way. (Luckily, it’s very rare.)

Again GCC and Clang reject this code, but MSVC accepts it:

template <typename T>
class propagate_const
{
public:
    using element_type
        = std::remove_reference_t<decltype(*std::declval<T>())>;

    operator element_type *()
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<T, element_type *>);

    operator const element_type *() const
      requires (std::is_pointer_v<T> 
                || std::is_convertible_v<const T, const element_type *>);
};

propagate_const<SomeClass *> ptr;

delete ptr; // ERROR on GCC, Clang; OK on MSVC

It is not entirely clear to me which compiler is right, here. A delete expression requires us to convert its argument to a pointer type, using what the Standard defines as a contextual implicit conversion. Basically, the compiler needs to search for a conversion operator that can convert a propagate_const to a pointer and find only one of such conversion operators. Yes, there are two available but shouldn’t overload resolution select a best one? According to MSVC, yes; according to GCC, no.

(Note that this wording has been introduced by N3323, which allegedly GCC does not fully implement. I have opened a bug report against GCC.)

Anyways, remember what we’ve just learned: we are allowed to perform pointer arithmetic! That means that we can “fix” these (rare) usages by deploying a unary operator plus:

propagate_const<SomeClass *> ptr;

delete +ptr; // OK! (ewww...)

That is pretty awful. I’ve decided instead to take my time and, in my project, refactor those “raw delete” by wrapping smart pointers instead:

propagate_const<std::unique_ptr<SomeClass>> ptr;  // better

A Way Forward

In order to support propagate_const on all compilers, and work around the limitations of the upstream implementations explained above, I have reimplemented propagate_const in KDToolBox, KDAB’s collection of miscellaneous useful C++ classes and stuff. You can find it here.

I’ve, of course, also submitted bug reports against libstdc++ and libc++. It was promptly fixed in GCC (GCC 13 will ship with the fix). Always report bugs upstream!

The Results

  • You can start using propagate_const and similar wrappers in your Qt projects, today. Some source incompatibilities are unfortunately present, but can be mitigated.
  • An implementation of propagate_const is available in KDToolBox. You can use it while you wait for an upgraded toolchain with the bugs fixed. 🙂
  • C++17 costs more. I cannot emphasize this enough. Not using the latest C++ standards costs more in development time, design, and debugging. While both GCC and MSVC (and upstream Clang) have very good C++20 support, Apple Clang is still lagging behind.
  • SFINAE on conversion operators is user-hostile. The workarounds are even worse. Use concepts and constraints instead.

Thank you for reading!

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 QObjects, Ownership, propagate_const and C++ Evolution appeared first on KDAB.

Can You Charge for Open-Source Software? Making Money from Open-Source Projects

The use of the term "free software" to describe software that is published under open-source licenses has often led to confusion over whether or not developers can actually charge money for their work.

Indeed, this confusion came up in one of the few litigated court cases in the United States involving open-source licenses. In 2006, a federal appeals court in Chicago dismissed a lawsuit brought by a developer -- Williams v. International Business Machines Corp. -- who alleged that various companies and entities responsible for developing Linux had engaged in a massive antitrust conspiracy to "eliminate competition in the operating system market by making Linux available at an unbeatable price," i.e., for nothing.

Frank Easterbrook, the judge who authored the appeals court's decision, explained that the GNU General Public License (GPL) "sets a price of zero" for covered works but that did not qualify as an illegal "price fixing" agreement under U.S. antitrust law. The flaw in Easterbrook's reasoning, however, was that the GPL says nothing of the sort. To the contrary, Section 4 of version 3 of the GPL expressly states:

You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. (Source)

So why did Easterbrook say the GPL sets a price of "zero"? The likely explanation was that in the context of a U.S. appeals court proceeding, Easterbrook took the allegations of the person who filed the lawsuit at face value. The developer who alleged the GPL violated antitrust law claimed the GPL mandated a zero-price, and the courts held that even assuming that was true, there was no antitrust violation since there was no harm to consumers.

Still, the fact that a judicial opinion authored by a prominent U.S. jurist stated the price of GPL-covered works is zero by design reflects an ongoing misunderstanding of how open-source licenses actually work. As we discussed in a prior article, the main goal of reciprocal open-source licenses like the GPL is to ensure that source code remains free and available to all users. But that isn't the same thing as saying an individual developer must provide their code--or modifications to existing code--without charging for it.

Applying Old Rules to Modern Software

It is important to note that the original GPL was published back in 1989. The software market back then was quite different than it is now. For one thing, most software was distributed on physical media like floppy disks and later optical discs or compact discs (CD). Most of that software only contained pre-compiled binary files without any source code. This included a lot of software that was distributed free of charge. Today, of course, most of our software comes from Internet sources.

One thing that may have been lost in this transition is an understanding of charging for open-source software as a normal business practice. To illustrate my point, a few years ago I was at a sale of used books held by my local public library. There was a small section of the sale dedicated to old computer books and media. It turned out that I found a sealed, boxed copy of Corel Linux, a long-forgotten Linux distribution first published in 1999. In addition to a hefty user's manual, the box contained two CD-ROMs. The first contained the pre-compiled version of Corel Linux to install on a computer. The second was the source code for all the files on the first CD-ROM.

This was not an uncommon practice back in the day when you would actually buy Linux distributions at a store. To comply with GPL or similar licensing requirements, the distributor would simply publish a separate CD-ROM with all of the source code. And if you look at section 6 of the current GPL, it states compliance may still be accomplished by distributing object code on a physical medium "accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange." But for most modern developers, it is far more practical to offer access to source code through an online repository on Github, Gitlab, or any other source code hosting service than to provide physical media.

Charging for Software Under the GPL and Other Open-Source Licenses

So what does the GPL actually allow -- or prohibit -- with respect to charging for software? Here is a brief rundown of some key provisions:

  • You can charge any price you wish -- including zero -- for any copy of a licensed work that you convey in either source or binary form.
  • You can charge for providing support for licensed code.
  • You can charge for providing a warranty for licensed code.
  • You may not charge a separate licensing fee or royalty to anyone who exercises their rights to use or convey GPL-licensed code, nor can you require them to collect such fees or royalties on your behalf.
  • You must still provide access to the accompanying source code if you convey GPL-licensed code in binary form.
  • You can charge for the "reasonable cost" of preparing and distributing a physical media if you provide source code in this kind of format.
  • You can not charge to allow access to the source code on an online server -- i.e., on the Internet --

Now, all of these provisions are specific to the GPL. What about non-reciprocal open-source licenses? In general, those licenses contain no restrictions on the sale or distribution of code. For example, the Python Software Foundation License says nothing about when you can and can not charge for software developed with Python. You are free to develop an application using Python and charge for it without even needing to provide access to your source code. The flip side of that, however, is that if you "mix" GPL and non-GPL code, the GPL's provisions will likely still apply to the entire work.

Another thing to consider when developing a commercial application is that while open-source licenses typically address issues related to copyrights in the underlying source code, they often do not cover other intellectual property rights attached to an application such as trademarks, which will be the subject of my next article.

Introducing kdalgorithms

Are you already convinced and just want to download the library? We’re glad you liked it: https://github.com/KDAB/KDAlgorithms

I’m sure many people have told you time and time again, Don’t use raw for loops; use algorithms.” Still, each time you tried, you found your code harder to read.

// ugly and unreadable algorithm code
vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
std::for_each(vec.cbegin(), vec.cend(), [](int i) { cout << i << " "; });

In case you’re wondering, the above simply prints out the vector.

Further, you might have looked at what C++20 (and especially C++23) offers in this area, but you are unfortunately stuck on C++17 or even C++14 and expect to be for quite a while.

Finally, you might have tried some of the existing libraries but found that they didn’t work very well with the Qt containers.

This was exactly my situation when I decided to write a few algorithm wrappers. Actually, I was using some other code already but it was GPL, and I wanted to offer something for Qt Widgets and More in an episode on algorithms.

In the rest of this blog post, I’ll switch between Qt and non-code Qt. KDAlgorithms works just as well with both container libraries. If you’re not familiar with Qt, just mentally replace QVector with std::vector, qDebug() with cout, and you will be just fine.

Let’s see a realistic (though still trivial) example of a loop that benefits from being rewritten as an algorithm:

 
QVector<QString> result; 
for (auto i : vec) { 
    result.append(QString::number(i)); 
} 
qDebug() << "transform with loop:" << result;

So what does the above do? It takes a vector of integers and converts it into a vector of strings — easy enough. Still, you need to read all 5 lines to understand what is going on.

Let’s see the same thing, this time with the STL algorithms available in C++14:

 
QString toString(int i) 
{ 
   return QString::number(i); 
}

QVector<QString> result; 
std::transform(vec.cbegin(), vec.cend(), std::back_inserter(result), toString); 
qDebug() << "Transform with std::transform:" << result;

I doubt many of you are running around the living room with your arms in the air screaming with joy.

One problem with the above is that you need to declare the result variable by itself and then use std::back_inserter to append to it. Another problem is that you need to call .cbegin() and .cend(), rather than just provide the vector. Finally, I’m sure a few of you are slightly bothered by the standalone toString function, which was what things looked like before C++11 and what lambdas fortunately solved.

Let me show you the above (this time with a lambda), rewritten to use kdalgorithms:

auto toString = [](int i) { return QString::number(i); }; 
auto result = kdalgorithms::transformed(vec, toString); 
qDebug() << "Transform to std::vector:" << result;

Now is a good time for that victory dance. Let me read the code for you:

  1. Okay, so there is a lambda here for converting an integer to a string. Well, I’m too important and busy to read the details — next!
  2. Aaah…okay, so this is transforming the vector by calling toString on each item.

Done!

You see, that is the beauty of algorithms (besides ensuring that the algorithm is properly implemented and tested) — the code is much easier to read. Heck, if you were really busy, you could have stopped at the word transformed and still got the gist of what is going on.

While the above few statements are true about algorithms in general, my claim is that they are even more so for kdalgorithms. Observe how we cut down on the extra noise:

  • We just provide the complete vector, not a pair of iterators. While there indeed are many good use cases for operating on a subset of a container, my guess is that in 97.8% of the cases you execute the algorithms on the full container.
  • You get the result returned to you, rather than providing an output iterator (std::back_inserter(result)) to where the data should go.

Okay, Tell Me What’s in the Package…

Sorry, I am not going to list all the available functions. For that, have a look at the documentation.

Let me, however, show you a few examples. For each of the examples, I’ll tell you, below the code, what it does, so you can try to interpret the code first.

Example: Find_if

 
std::vector vec{1, 2, 3, 4, 5};
auto greaterThan = [] (int value) { 
  return [value] (int test) { 
    return test > value; 
  }; 
};
const auto result = kdalgorithms::find_if(vec, greaterThan(2)); 
if (result) 
   std::cout << *result << std::endl; 
else 
   std::cout << "ahh nothing, right\n";

That wasn’t too hard, was it? First, we created a nifty function that returns another function. If you can’t wrap your head around that, it basically just boils down to writing this code in the find_if line:

 
const auto result = kdalgorithms::find_if(vec, [] (int test) { return test > 2; });

The find_if basically searches for the first number in the list that’s greater than 2. However, in contrast to STL algorithms, it doesn’t return an iterator but, rather, an object with an operator bool() so it’s more natural to check if there was a result, and an operator* for getting to the actual value.

See more about find_if in the documentation.

Example: Partitioned

 
const auto& [alive, dead] = 
   kdalgorithms::partitioned<std::unordered_set>(getAllPlayers(), 
                                                 &Player::isAlive);

Simple, still quite a few things to explain.

First, kdalgorithms::partitioned returns a simple struct with two containers in it — one called in, the other called out. Using C++17’s structured binding, I unpack that struct as the value is returned. Had I only had C++14, the code would look like this:

 
const auto result = 
    kdalgorithms::partitioned<std::unordered_set>(getAllPlayers(), &Player::isAlive);
const auto in = result.in;
const auto out = result.out;

Second, the method getAllPlayers() — which honestly only exists in my head — returns a std::vector<Player>. Maybe the players are sorted somehow, but I don’t care about sorting. Instead, the following code might want to do some set operations, so, on the fly, kdalgorithms::partitioned converts the result into two unordered sets. Had I not had that requirement, I could have left out the <std::unordered_set> part and kdalgorithms::partitioned would return a structure containing two vectors.

Finally, what kdalgorithms::partitioned is doing is to splitting the input into two collections — one that contains values that match the given predicate, and one that contains those that don’t (the in‘s and out‘s). As mentioned, the input is a vector of Players and, rather than writing a lambda to call isAlive on each item to decide if they are in or out, I simply provide a pointer to the member function.

In other words, the above code would produce the same result as this:

 
const auto& [alive, dead] = 
   kdalgorithms::partitioned<std::unordered_set>(
      getAllPlayers(), [] (const Player& player) { return player.isAlive(); });

By the way, the syntax &Player::isAlive would be the same whether isAlive is a public function or a public member variable.

Want more examples of partitioned? Head over to the documentation for partitioned.

Example: Searching for Old People

 
std::vector<std::string> names{"Jesper", "Anne Helene", "Louise", "Laura", "Santa"};
std::vector<int> ages{52, 49, 11, 8};

auto olderThan = [](int age) {
   return [age](const auto &tuple) { return std::get<1>(tuple) > age; };
};
auto getName = [](const auto &tuple) { return std::get<0>(tuple); };
auto oldPeople = kdalgorithms::filtered_transformed(kdalgorithms::zip(names, ages),
                                                    getName, olderThan(40));

Ahem! It’s senior citizens, not old people. Besides that, I take it the code reads without any hiccups, right?

Still let me go over it.

The first two lines are just setting up the test data. And I’m sure you are already asking me why I didn’t just have a vector of structs? To that, my answer is simple: Oh for <beep>’s sake please get down from the ivory tower.

We’ve all been in that situation where we have two pieces of data representing different attributes of the same items (here, name and age) but, unfortunately, we’ve had them in two different containers.

Reviewing your code, I’m sure you will find code that smells like this:

 
for (nameIt = names.cbegin(), ageIt = ages.cbegin();
     namesIt != names.cend() && agesIt != ages.cend();
     ++namesIt, ++agesIt) {
  ....
}

That’s where kdalgorithms::zip (which you may get from the file kdalgorithms.zip — okay, I admit that was a bad joke!) comes to the rescue. It takes any number of containers and produces a resulting container of tuples, each containing one item from each container.

The above test data would result in this:

 
std::vector<std::tuple<std::string, int>> resultOfZip{
    {"Jesper", 52},
    {"Anne Helene", 49},
    {"Louise", 11}, 
    {"Laura", 8} 
};

Next, we use filtered_transformed, which, despite its terrible name, does a very common thing:

First, it filters the data (here, it searches for everyone above 40, using the function olderThan(40)) and then transforms those that match (here, it’s calling getName to extract the name from the tuple). The result is a std::vector with two members in it, namely “Jesper” and “Anne Helene” (which, incidentally, is my wife and me — though I’m not saying she’s old!)

Want to learn more about zip or filtered_transformed? <—-Well, there are two links to the documentation.

So, Why Did You Create kdalgorithms?

As previously mentioned, I wanted to showcase algorithms in Qt Widgets and More. Additionally, and maybe most importantly, I wanted to learn much more about template meta programming.

As everyone else in KDAB, I’m entitled to utilizing 10% of my work time for education purposes. So, I decided to play with templates.

So, you might ask, “Is this just a piece of play code for you that you will abandon in no time?”

No! In KDAB, I also wrote my own version of what the rest of the world would think of as SAP — namely, a pretty large and complex piece of software that can tell KDAB everything we want to know about our cashflow, our customers, and even who’s been a bad boy and worked a lot of overtime recently (something we do not allow people to do; we insist that they have a good work/life balance).

What was I talking about? Ahh…right. Yes, in that tool, which is 150,000 lines of code, I am using kdalgorithms intensively –  a word count on kdalgorithms:: reveals almost 500 calls.

So, unless I one day decide that I need a bus full of consultants to redesign KDAB (by introducing SAP), I likely am doomed to support kdalgorithms for a very long time.

Which, now that I think of it, is great, since templates are a lot of fun and I’m sure that, just like 3-dimensional sudokus will delay Alzheimers, so will template meta programming.

Want more? Okay, then — you have to look me in the eyes (aka. watch youtube videos of me). Episode 95 is about kdalgorithms and shows a few different examples. Episodes 96-99 dive into a lot of template meta programming code, with the goal of letting the viewer understand how kdalgorithms::transformed is implemented and, more specifically, optimized for temporaries (that is rvalues) where it can steal the container provided.

Final words: Time to download and provide your feedback on kdalgorithms.

A Special Thanks…

I’d like to bring a special thank you to my coworker, Ivan Čukić, who has been a tremendous help on my journey towards a reborn C++ developer.

Ivan has patiently reviewed all of my pitiful attempts to write template code and continuously pushed me to reach higher.

Besides being a brilliant C++ developer, Ivan also has a good eye for structuring your code for maximum readability. To that end, I can strongly recommend his book on functional programming in C++.

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 Introducing kdalgorithms appeared first on KDAB.

PyQt QCheckBox Widget — Add toggleable Config & Preferences checkbox options

A checkbox is a square-shaped widget used in graphical user interfaces (GUI) to provide users with a way to enable or disable options, or to allow users to enable or disable certain features of the app. Checkboxes usually have an adjacent label, indicating what the checkbox represents.

Checkboxes are usually used to represent boolean state, being either checked (on) or unchecked (off). However, you can also have tri-state checkboxes with a third indeterminate state. In this tutorial, you'll learn how to create and customize checkboxes in your own applications, using the QCheckBox class.

Creating Checkbox Widgets With QCheckBox

The QCheckBox class represents a checkbox widget. A checkbox is an option widget that can be switched on (checked) or off (unchecked) by the user. This widget is common in settings or preferences dialogs where the user fine-tunes an application's configuration.

Checkboxes typically have a square shape looking like a checkbox on a paper form. We can click the box to switch it on and click it again to switch it off. When checked a cross or checkmark symbol will appear in the box. When a checkbox is unchecked, it will appear as an empty square.

If you're using PyQt or PySide to create GUI applications in Python, then you can add checkboxes to your application with the QCheckBox class. This class provides two different constructors that we can use to create checkboxes:

Constructor Description
QCheckBox(parent: QWidget = None) Constructs a checkbox with an optional parent widget but without an associated text
QCheckBox(text: str, parent: QWidget = None) Constructs a checkbox with an associated description text and an optional parent widget

To illustrate how to use the above constructors, let's get into our first example:

python
import sys

from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Checkbox with no label
        noTextCkBox = QCheckBox()
        # Checkbox with a text label
        textCkBox = QCheckBox(text="Option")
        layout = QVBoxLayout()
        layout.addWidget(noTextCkBox)
        layout.addWidget(textCkBox)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Checkbox with no label
        noTextCkBox = QCheckBox()
        # Checkbox with a text label
        textCkBox = QCheckBox(text="Option")
        layout = QVBoxLayout()
        layout.addWidget(noTextCkBox)
        layout.addWidget(textCkBox)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we import QCheckBox from the QtWidgets module. We create two QCheckBox instances in the __init__ method of the Window class. The first is created using the constructor that does not require a text label.

Both constructors take an optional parent widget, but since we are adding the checkbox widgets to a layout, we don't need to pass a parent when contructing them.

Next up, we use the second constructor of QCheckBox to create another checkbox, this time with descriptive label. The text will display on the right of the checkbox. The text is a regular Python string. This label would typically be used to describe the option to be enabled or disabled, but here is just "Option".

Save this code to a .py file and run it. You'll get a window that looks something like this:

QCheckBox constructors

As expected, the first checkbox has no associated text. It's just a square box on the window. This isn't too helpful, because it doesn't provide any information to your users. However, it can be useful when creating an array of checkboxes laid out to present some data in a table or a list.

The second checkbox on the window looks more familiar -- it has an associated text.

Both checkboxes are fully functional. We can check and uncheck them by clicking on them or -- in the case of the second example -- on the label. However, the checkboxes don't do any action yet, they just change their internal state from checked to unchecked and the other way around. To give a checkbox purpose we need to check the state and take action accordingly ourselves.

In most UIs checkboxes typically don't trigger external actions when toggled. Use buttons to launch or trigger things.

Normal checkboxes have two internal states Checked and Unchecked, while tri-state checkboxes add a third PartiallyChecked state. Next we'll look at how to check and set these states on both two-state and tri-state checkboxes.

Getting and Setting Checkbox state

The most important property of a checkbox is its internal state. This holds whether the checkbox is checked or not. By checking this property, you can take appropriate action in your application.

There are two main methods for checking the checkbox internal state -- isChecked() and checkState(). The possible values returned for a two-state checkbox are shown below:

Methods Behavior
isChecked() Will return True if checkbox is checked, False if it is unchecked.
checkState() In two-state checkboxes this will return either Checked and Unchecked.

Consider the following example:

python
import sys

from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.checkBox = QCheckBox(text="Unchecked")
        layout = QVBoxLayout()
        layout.addWidget(self.checkBox)
        self.setLayout(layout)
        self.checkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        if self.checkBox.isChecked():
            self.checkBox.setText("Checked")
        else:
            self.checkBox.setText("Unchecked")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.checkBox = QCheckBox(text="Unchecked")
        layout = QVBoxLayout()
        layout.addWidget(self.checkBox)
        self.setLayout(layout)
        self.checkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        if self.checkBox.isChecked():
            self.checkBox.setText("Checked")
        else:
            self.checkBox.setText("Unchecked")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we first create a QCheckBox instance with the text "Click me!" on it. In this case, we're defining a two-state checkbox, which represents the default behavior of this widget.

Then we create the app's layout and add the checkbox. Finally, we connect the stateChanged() signal with the onStateChanged() slot or method. This signal is emitted whenever we click the checkbox, and the onStateChanged() method is automatically called.

You can use the stateChanged() signal to trigger something to happen each time the checkbox changes its state. To connect the stateChanged() signal to a slot, you use the standard syntax:

python
checkbox.<signal>.connect(<method>)

In this construct, checkbox is the QCheckBox object we need to connect to a given slot. The <signal> name is stateChanged. Finally, <method> represents the target slot or method we want to connect the signal.

The onStateChanged() method uses isChecked() to determine the current state of our checkbox. This method returns True if the checkbox is checked and False if it's unchecked. The checkbox text changes alternatively from Checked to Unchecked depending on the result of this condition:

Two-state checkbox

Whenever you click the checkbox on this window, the text changes from Checked to Unchecked alternatively.

It's usually not a good idea to change a checkbox label with the state as it's confusing. Does the "Unchecked" label next to a unchecked checkbox mean checked or unchecked?

We could have used checkState() to determine the current state of our checkbox in the above example. However, using isChecked() feels more natural when working with two-state checkboxes.

When we are working with tri-state checkboxes, checkState() is our only option. We'll see how to work with checkState() in the next section.

Building and Working With Tri-State Checkboxes

In addition to regular two-state checkboxes, you can optionally create tri-state checkboxes. This type of checkbox comes in handy when we want to give our user the option to set the checkbox in an intermediate state known as a partially checked state. This state means that the checkbox is neither checked nor unchecked.

Tri-state checkboxes are typically used to represent groups of other checkboxes, allowing them to all be turned on or off in one go. The partially checked state then indicates that some of the children are on and some are off.

When using tri-state checkboxes, the isChecked() and checkState() methods return the following values --

Methods Behavior
isChecked() Will return True if checkbox is checked or partially checked, False if it is unchecked.
checkState() In three-state checkboxes this will return either Checked, Unchecked or PartiallyChecked .

The QCheckBox class has a Boolean tristate property that indicates whether the checkbox is tri-state. This property defaults to False. If we set it to True, we turn our checkbox into a tri-state checkbox. To change the value of tristate, you need to use the setTristate() with a boolean value as an argument.

You can also turn a checkbox tri-state by setting a partially checked state on it, using .setState(Qt.CheckState.PartiallyChecked).

Consider the following example:

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.triStateCheckBox = QCheckBox(text="Unchecked")
        self.triStateCheckBox.setTristate(True)
        layout = QVBoxLayout()
        layout.addWidget(self.triStateCheckBox)
        self.setLayout(layout)
        self.triStateCheckBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        state = self.triStateCheckBox.checkState()
        if state == Qt.CheckState.Checked:
            self.triStateCheckBox.setText("Checked")
        elif state == Qt.CheckState.PartiallyChecked:
            self.triStateCheckBox.setText("Partially checked")
        else:
            self.triStateCheckBox.setText("Unchecked")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QCheckBox,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.triStateCheckBox = QCheckBox(text="Unchecked")
        self.triStateCheckBox.setTristate(True)
        layout = QVBoxLayout()
        layout.addWidget(self.triStateCheckBox)
        self.setLayout(layout)
        self.triStateCheckBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        state = self.triStateCheckBox.checkState()
        if state == Qt.CheckState.Checked:
            self.triStateCheckBox.setText("Checked")
        elif state == Qt.CheckState.PartiallyChecked:
            self.triStateCheckBox.setText("Partially checked")
        else:
            self.triStateCheckBox.setText("Unchecked")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create a regular checkbox using the QCheckBox() constructor. To set this checkbox as tri-state, you call .setTristate() with True as an argument. From now on, our checkbox will have three states represented by the following values:

Constant Value Description
Qt.CheckState.Unchecked 0 The checkbox is unchecked.
Qt.CheckState.PartiallyChecked 1 The checkbox is partially checked.
Qt.CheckState.Checked 2 The checkbox is checked.

In tri-state checkboxes, the isChecked() method will return True whether the checkbox is checked or partially checked, and False if it is unchecked.

If you want to distinguish between checked and partially checked states, you need to use checkState() directly. Calling checkState() returns one of the constants in the above table, representing the current state of the checkbox.

Our onStateChanged() method is connected to the stateChanged signal of the checkbox. In this method, we get the current state by calling checkState() on the checkbox and storing the result in the variable state. We then check the value of this variable against the different states and take action accordingly. In this example, we alternatively change the checkbox text to reflect its state.

Here's how this example works in practice:

Tri-state checkbox

When you click the checkbox, its text changes from Unchecked to Partially checked and finally to Checked. These are the three possible states of this kind of checkbox.

While we used the checkState() method above, the stateChanged signal actually sends the current state, as an integer value, to the slot. We can map that into the correct state in the method directly by passing it to Qt.CheckState. For example.

python
    def onStateChanged(self, state):
        state = Qt.CheckState(state)
        if state == Qt.CheckState.Checked:
            self.triStateCheckBox.setText("Checked")
        elif state == Qt.CheckState.PartiallyChecked:
            self.triStateCheckBox.setText("Partially checked")
        else:
            self.triStateCheckBox.setText("Unchecked")

In this alternative implementation of onStateChanged(), we receive a state argument that we map to a Qt.CheckState object. The rest of the code remains the same.

The mapping step is only required in PyQt6. In both PyQt5, PySide2, and PySide6 this is done automatically.

It's up to you which implementation to use in your code.

Using Checkboxes in Practice

We can use checkbox widgets to provide a clean and user-friendly way to enable or disable options in a GUI app. Checkboxes are useful when building preferences or setting dialogs where the users can customize our applications. For example, say that you're making a text editor and you want to allow the user to customize a few features like:

  • Show word count
  • Auto pair brackets
  • Show minimap

Checkbox options shouldn't usually be mutually exclusive. If you have mutually exclusive options, consider using the QRadioButton class instead.

In this case, you can use a checkbox for each of these options like in the following code:

python
import sys

from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QLabel,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.wordCountCkBox = QCheckBox(text="Show word count", parent=self)
        self.bracketsCkBox = QCheckBox(text="Auto pair brackets", parent=self)
        self.minimapCkBox = QCheckBox(text="Show minimap", parent=self)
        self.selectionLabel = QLabel(text="No selection yet", parent=self)
        layout = QVBoxLayout()
        layout.addWidget(self.wordCountCkBox)
        layout.addWidget(self.bracketsCkBox)
        layout.addWidget(self.minimapCkBox)
        layout.addWidget(self.selectionLabel)
        self.setLayout(layout)
        self.wordCountCkBox.stateChanged.connect(self.onStateChanged)
        self.bracketsCkBox.stateChanged.connect(self.onStateChanged)
        self.minimapCkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        selection = "You've selected:\n"
        if self.wordCountCkBox.isChecked():
            selection += "- Word count\n"
        if self.bracketsCkBox.isChecked():
            selection += "- Auto pair brackets\n"
        if self.minimapCkBox.isChecked():
            selection += "- Show minimap\n"
        self.selectionLabel.setText(selection)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import (
    QApplication,
    QCheckBox,
    QLabel,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.wordCountCkBox = QCheckBox(text="Show word count", parent=self)
        self.bracketsCkBox = QCheckBox(text="Auto pair brackets", parent=self)
        self.minimapCkBox = QCheckBox(text="Show minimap", parent=self)
        self.selectionLabel = QLabel(text="No selection yet", parent=self)
        layout = QVBoxLayout()
        layout.addWidget(self.wordCountCkBox)
        layout.addWidget(self.bracketsCkBox)
        layout.addWidget(self.minimapCkBox)
        layout.addWidget(self.selectionLabel)
        self.setLayout(layout)
        self.wordCountCkBox.stateChanged.connect(self.onStateChanged)
        self.bracketsCkBox.stateChanged.connect(self.onStateChanged)
        self.minimapCkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self):
        selection = "You've selected:\n"
        if self.wordCountCkBox.isChecked():
            selection += "- Word count\n"
        if self.bracketsCkBox.isChecked():
            selection += "- Auto pair brackets\n"
        if self.minimapCkBox.isChecked():
            selection += "- Show minimap\n"
        self.selectionLabel.setText(selection)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create three checkboxes, one per option. We also create a label object to show the user's selections. If you run this application, then you'll get the following behavior:

Checkboxes for text editor settings

This dialog shows the three required options for your text editor. Of course, this dialog doesn't look like a fully functional setting or preferences dialog. It intends to show how you can use checkboxes to provide non-exclusive options to your users. Note that you can select any combination of options.

Setting Other Properties of QCheckBox

Up to this point, we've learned how to create two-state and tri-state checkboxes in your GUI applications. We've also learned how to make our checkboxes perform actions in response to state changes by connecting the stateChanged signals with concrete methods known as slots.

In this section, we'll learn about other useful features of QCheckBox, including the text and icon properties shown below:

Property Description Getter Method Setter Method
text Holds the text shown on the button text() setText()
icon Holds the icon shown on the button icon() setIcon()

As its name suggests, the text property controls the current text associated with a checkbox. You can use the setText() method to change this property and text() to retrieve its current value if needed. In previous sections, you used setText() to change the text of a checkbox. So, let's focus on how to work with icons.

Here's an example of a checkbox representing a traffic light in which each state will have its own associated icon:

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QCheckBox, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.lightCkBox = QCheckBox(parent=self)
        self.lightCkBox.setTristate(True)
        self.lightCkBox.setIcon(QIcon("icons/red.png"))
        layout = QVBoxLayout()
        layout.addWidget(self.lightCkBox)
        self.setLayout(layout)
        self.lightCkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self, state):
        state = Qt.CheckState(state)
        if state == Qt.CheckState.Checked:
            self.lightCkBox.setIcon(QIcon("icons/green.png"))
        elif state == Qt.CheckState.PartiallyChecked:
            self.lightCkBox.setIcon(QIcon("icons/yellow.png"))
        else:
            self.lightCkBox.setIcon(QIcon("icons/red.png"))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QCheckBox, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.lightCkBox = QCheckBox(parent=self)
        self.lightCkBox.setTristate(True)
        self.lightCkBox.setIcon(QIcon("icons/red.png"))
        layout = QVBoxLayout()
        layout.addWidget(self.lightCkBox)
        self.setLayout(layout)
        self.lightCkBox.stateChanged.connect(self.onStateChanged)

    def onStateChanged(self, state):
        state = Qt.CheckState(state)
        if state == Qt.CheckState.Checked:
            self.lightCkBox.setIcon(QIcon("icons/green.png"))
        elif state == Qt.CheckState.PartiallyChecked:
            self.lightCkBox.setIcon(QIcon("icons/yellow.png"))
        else:
            self.lightCkBox.setIcon(QIcon("icons/red.png"))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())


In this example, we use the setIcon() method to change the icon associated with the checkbox according to its current state. Go ahead and run the app! You'll get a window like the following:

Checkbox with icons

In this example, the red light is associated with the unchecked state, the yellow light is linked to the partially checked state, and the green light is associated with the checked state.

Using icons like in this example is something rare with checkbox widgets. You'll typically use descriptive text. But it's good to know the capability is there for when you need it.

Conclusion

Checkboxes are useful widgets in any GUI application. They allow us to give our users a clean way to enable or disable options in our applications. They are commonly used in dialogs and preferences windows to allow enabling and disabling of application features or configuring behavior.

In this tutorial, you've learned how to create, use, and customize checkboxes while building a GUI application with PyQt.

For more, see the complete PySide2 tutorial.

Happy 9th birthday, qutebrowser!

qutebrowser is turning 9 today! I’ll use the opportunity for a – perhaps slightly tl;dr – overview of how it all came to be. As you might notice by the length of this post, stopping to write once I started writing something like this… isn’t exactly my forte! Hopefully …

PyQt QPushButton — A universal Button Widget

The push button, or command button, is perhaps the most commonly used widget in any graphical user interface (GUI). A button is a rectangular widget that typically displays a text describing its aim.

When we click a button, we command the computer to perform actions or to answer a question. Typical buttons include Ok, Apply, Cancel, Close, Yes, No and Help. However, they're not restricted to this list.

In this tutorial, you'll learn how to create and customize button widgets in your GUI applications.

Creating Buttons Widgets With QPushButton

Buttons are probably the most common widgets in GUI applications. They come in handy when you need to create dialogs for communicating with your users. You'll probably be familiar with Accept, Ok, Canel, Yes, No, and Apply buttons, which are commonplace in modern graphical user interfaces.

In general, buttons allow you to trigger actions in response to the user's clicks on the button's area. Buttons often have a rectangular shape and include a text label that describes their intended action or command.

If you've used PyQt or PySide to create GUI applications in Python, then it'd be pretty likely that you already know about the QPushButton class. This class allows you to create buttons in your graphical interfaces quickly.

The QPushButton class provides three different constructors that you can use to create buttons according to your needs:

Constructor Description
QPushButton(parent: QWidget = None) Constructs a button with a parent widget but without text or icon
QPushButton(text: str, parent: QWidget = None) Constructs a button with a parent widget and the description text but without an icon
QPushButton(icon: QIcon, text: str, parent: QWidget = None) Constructs a button with an icon, text, and parent widget

To illustrate how to use the above constructors, you can code the following example:

python
import sys

from PyQt6.QtCore import QSize
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Button with a parent widget
        topBtn = QPushButton(parent=self)
        topBtn.setFixedSize(100, 60)
        # Button with a text and parent widget
        centerBtn = QPushButton(text="Center", parent=self)
        centerBtn.setFixedSize(100, 60)
        # Button with an icon, a text, and a parent widget
        bottomBtn = QPushButton(
            icon=QIcon("./icons/logo.svg"),
            text="Bottom",
            parent=self
        )
        bottomBtn.setFixedSize(100, 60)
        bottomBtn.setIconSize(QSize(40, 40))
        layout = QVBoxLayout()
        layout.addWidget(topBtn)
        layout.addWidget(centerBtn)
        layout.addWidget(bottomBtn)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Button with a parent widget
        topBtn = QPushButton(parent=self)
        topBtn.setFixedSize(100, 60)
        # Button with a text and parent widget
        centerBtn = QPushButton(text="Center", parent=self)
        centerBtn.setFixedSize(100, 60)
        # Button with an icon, a text, and a parent widget
        bottomBtn = QPushButton(
            icon=QIcon("./icons/logo.svg"),
            text="Bottom",
            parent=self
        )
        bottomBtn.setFixedSize(100, 60)
        bottomBtn.setIconSize(QSize(40, 40))
        layout = QVBoxLayout()
        layout.addWidget(topBtn)
        layout.addWidget(centerBtn)
        layout.addWidget(bottomBtn)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

You can download the logo here or use your own image file. You can also use PNG format images if you prefer.

This code does a lot! First, we do the required imports. Inside our Window class, we create three QPushButton instances. To create the first button, we use the first constructor of QPushButton. That's why we only pass a parent widget.

For the second button, we use the second constructor of QPushButton. This time, we provide the button's text and parent. Note that the text is a regular Python string.

Our last button uses the third constructor. In this case, we need to provide the button's icon, text, and parent. We use the QIcon class with an SVG image as an argument to provide the icon. Note that we can also use a QPixmap object as the button's icon.

Save this code to a .py file and run it. You'll get a window that looks something like this:

QPushButton constructors example, showing three buttons with labels & icon QPushButton constructors example, showing three buttons with labels & icon

The first button has no text. It's just a rectangular shape on the app's windows. The second button in the center has text only, while the third button at the bottom of the window has an icon and text. That looks great!

These buttons don't do any action jet. If you click them, then you'll realize that nothing happens. To make your buttons perform concrete actions, you need to connect their signals to some useful slots. You'll learn how to do this in the next section.

Connecting Signals and Slots

Depending on specific user events, the QPushButton class can emit four different signals. Here's is a summary of these signals and their corresponding meaning:

Signal Emitted
clicked(checked=false) When the user clicks the button and activates it.
pressed() When the user presses the button down.
released() When the user releases the button.
toggled(checked=false) Whenever a checkable button changes its state from checked to unchecked and vice versa.

When you're creating a GUI application, and you need to use buttons, then you need to think of the appropriate signal to use. Most of the time, you'll use the button's clicked() signal since clicking a button is the most common user event you'll ever need to process.

The other part of the signal and slot equation is the slot itself. A slot is a method or function that performs a concrete action in your application. Now, how can you connect a signal to a slot so that the slot gets called when the signal gets emitted? Here's the required syntax to do this:

python
button.<signal>.connect(<method>)

In this construct, button is the QPushButton object we need to connect with a given slot. The <signal> placeholder can be any of the four abovementioned signals. Finally, <method> represents the target slot or method.

Let's write an example that puts this syntax into action. For this example, we'll connect the clicked signal with a method that counts the clicks on a button:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.count = 0
        self.button = QPushButton(f"Click Count: {self.count}", self)
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.count_clicks)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def count_clicks(self):
        self.count += 1
        self.button.setText(f"Click Count: {self.count}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.count = 0
        self.button = QPushButton(f"Click Count: {self.count}", self)
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.count_clicks)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def count_clicks(self):
        self.count += 1
        self.button.setText(f"Click Count: {self.count}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

The button variable holds an instance of QPushButton. To create this button, we use a text and parent widget. This parent widget works as our current window. Then we connect the button's clicked signal with the count_clicks() method.

The count_clicks() method counts the number of clicks on the button and updates the button's text accordingly. Go ahead and run the app!

Exploring the Public API of QPushButton

Up to this point, you've learned about the different ways to create buttons in your GUI applications. You've also learned how to make your buttons perform actions in response to the user's actions by connecting the buttons' signals with concrete methods known as slots.

In the following sections, you'll learn about the QPushButton class's public API and its most useful properties, including the following:

Property Description Getter Method Setter Method
text Holds the text shown on the button text() setText()
icon Holds the icon shown on the button icon() setIcon()
shortcut Holds the keyboard shortcut associated with the button shortcut() setShortcut()

Let's kick things off by learning how to set and get a button's text, icon, and keyboard shortcut. These actions can be an essential part of your GUI design process.

Setting a Button's Text, Icon, and Shortcut

In previous sections, you've learned how to create buttons using different class constructors. Some of these constructors allow you to set the button's text directly. However, sometimes you need to manipulate a button's text at runtime. To accomplish this, you can use setText() and text().

As its name suggests, the setText() method allows you to set the text of a given button. On the other hand, the text() lets you retrieve the current text of a button. Here's a toy example of how to use these methods:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QPushButton("Hello!")
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def onClick(self):
        text = self.button.text()
        if text == "Hello!":
            self.button.setText(text[:-1] + ", World!")
        else:
            self.button.setText("Hello!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QPushButton("Hello!")
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def onClick(self):
        text = self.button.text()
        if text == "Hello!":
            self.button.setText(text[:-1] + ", World!")
        else:
            self.button.setText("Hello!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we use text() and setText() inside the onClick() method to manipulate the text of our button object. These methods come in handy when we need to set and retrieve a button's text at run time, which would be useful in a few situations. For example, if we need a button to fold and unfold a tree of widgets or other objects.

Go ahead and run the app! You'll get a window like the following:

Window with a single button, with the text Hello! Click to change the text. Window with a single button, with the text Hello! Click to change the text.

In this example, when you click the button, its text alternate between Hello! and Hello, World!.

QPushButton also has methods to manipulate the icon of a given button. In this case, the methods are setIcon() and icon(). You can set the button's icon at run time with the first method. The second method allows you to retrieve the icon of a given button. There's also a third method related to icons. It's called .setIconSize() and allows you to manipulate the icon size.

Here's an example that illustrates how to use these methods:

python
import sys

from PyQt6.QtCore import QSize
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(
            icon=QIcon("./icons/logo.svg"), text="Click me!", parent=self
        )
        self.btnOne.setFixedSize(100, 60)
        self.btnOne.clicked.connect(self.onClick)
        self.btnTwo = QPushButton(parent=self)
        self.btnTwo.setFixedSize(100, 60)
        self.btnTwo.setEnabled(False)
        self.btnTwo.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        self.setLayout(layout)

    def onClick(self):
        sender = self.sender()
        icon = sender.icon()

        if sender is self.btnOne:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnTwo.setEnabled(True)
            self.btnTwo.setText("Click me!")
            self.btnTwo.setIcon(icon)
            self.btnTwo.setIconSize(QSize(20, 20))
        elif sender is self.btnTwo:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnOne.setEnabled(True)
            self.btnOne.setText("Click me!")
            self.btnOne.setIcon(icon)
            self.btnOne.setIconSize(QSize(30, 30))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(
            icon=QIcon("./icons/logo.svg"), text="Click me!", parent=self
        )
        self.btnOne.setFixedSize(100, 60)
        self.btnOne.clicked.connect(self.onClick)
        self.btnTwo = QPushButton(parent=self)
        self.btnTwo.setFixedSize(100, 60)
        self.btnTwo.setEnabled(False)
        self.btnTwo.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        self.setLayout(layout)

    def onClick(self):
        sender = self.sender()
        icon = sender.icon()

        if sender is self.btnOne:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnTwo.setEnabled(True)
            self.btnTwo.setText("Click me!")
            self.btnTwo.setIcon(icon)
            self.btnTwo.setIconSize(QSize(20, 20))
        elif sender is self.btnTwo:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnOne.setEnabled(True)
            self.btnOne.setText("Click me!")
            self.btnOne.setIcon(icon)
            self.btnOne.setIconSize(QSize(30, 30))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create an app with two buttons. Both buttons are connected to the onClick() method. Inside the method, we first get the clicked button using the sender() method on our app's window, self.

Next, we get a reference to the sender button's icon using the icon() method. The if statement checks if the clicked object was btnOne. If that's the case, then we reset the icon with setIcon() and disable the button with setEnabled(). The following four lines enable the btnTwo button, set its text to "Click me!", change the button's icon, and resize the icon. The elif clause does something similar, but this time the target button is btnOne.

If you run this application, then you'll get a window like this:

Window with two buttons, the top with a icon & label, the bottom empty. Click to toggle which button has the label & icon._ Window with two buttons, the top with a icon & label, the bottom empty. Click to toggle which button has the label & icon.

When we click the top button, the bottom button's text and icon will be set to Click me! and to the PythonGUIs.com logo, respectively. At the same time, the top button's text and icon will disappear. Note that the logo's size will change as well.

Another useful feature of buttons is that you can assign them a keyboard shortcut for those users that prefer the keyboard over the mouse. These methods are .setShortcut() and .shortcut(). Again, you can use the first method to set a shortcut and the second method to get the shortcut assigned to the underlying button.

These methods are commonly helpful in situations where we have a button that doesn't have any text. Therefore we can't assign it an automatic shortcut using the ampersand character &.

Checking the Status of a Button

Sometimes you'd need to check the status of a given button and take action accordingly. The QPushButton class provides a few methods that can help you check different properties related to the current status of your buttons:

Property Description Access Method Setter Method
down Indicates whether the button is pressed down or not isDown() setDown()
checked Indicates whether the button is checked or not isChecked() setChecked()
enabled Indicates whether the button is enabled or not isEnabled() setEnabled()

The down status is typically transitory and naturally happens between the pressed and released statuses. However, we can use the setDown() method to manipulate the down status at runtime.

The checked status is only available when we use checkable buttons. Only checkable buttons can be at either the checked or unchecked status.

Finally, when we enable or disable a given button, we allow or disallow the user's click on the button. In other words, disabled buttons don't respond to the user's clicks or other events, while enabled buttons do respond.

Here's an example that shows how these three sets of statuses work:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="I'm Down!", parent=self)
        self.btnOne.setFixedSize(150, 60)
        self.btnOne.setDown(True)
        self.btnOne.clicked.connect(self.onBtnOneClicked)

        self.btnTwo = QPushButton(text="I'm Disabled!", parent=self)
        self.btnTwo.setFixedSize(150, 60)
        self.btnTwo.setEnabled(False)

        self.btnThree = QPushButton(text="I'm Checked!", parent=self)
        self.btnThree.setFixedSize(150, 60)
        self.btnThree.setCheckable(True)
        self.btnThree.setChecked(True)
        self.btnThree.clicked.connect(self.onBtnThreeClicked)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        layout.addWidget(self.btnThree)
        self.setLayout(layout)

    def onBtnOneClicked(self):
        if not self.btnOne.isDown():
            self.btnOne.setText("I'm Up!")
            self.btnOne.setDown(False)

    def onBtnThreeClicked(self):
        if self.btnThree.isChecked():
            self.btnThree.setText("I'm Checked!")
        else:
            self.btnThree.setText("I'm Unchecked!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="I'm Down!", parent=self)
        self.btnOne.setFixedSize(150, 60)
        self.btnOne.setDown(True)
        self.btnOne.clicked.connect(self.onBtnOneClicked)

        self.btnTwo = QPushButton(text="I'm Disabled!", parent=self)
        self.btnTwo.setFixedSize(150, 60)
        self.btnTwo.setEnabled(False)

        self.btnThree = QPushButton(text="I'm Checked!", parent=self)
        self.btnThree.setFixedSize(150, 60)
        self.btnThree.setCheckable(True)
        self.btnThree.setChecked(True)
        self.btnThree.clicked.connect(self.onBtnThreeClicked)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        layout.addWidget(self.btnThree)
        self.setLayout(layout)

    def onBtnOneClicked(self):
        if not self.btnOne.isDown():
            self.btnOne.setText("I'm Up!")
            self.btnOne.setDown(False)

    def onBtnThreeClicked(self):
        if self.btnThree.isChecked():
            self.btnThree.setText("I'm Checked!")
        else:
            self.btnThree.setText("I'm Unchecked!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we first set btnOne down using the setDown() method. Then we disable btnTwo using the setEnabled() with False as an argument. This will make this button unresponsive to user events. Finally, we set btnThree as checkable with setCheckable(). Being checkable means that we can use the checked and unchecked statuses in our code.

The onBtnOneClicked() method is connected to btnOne. This method checks if the button is not down and changes the button text accordingly.

The onBtnThreeClicked() is connected to btnThree. This method alternatively changes the button's text depending on its checked status.

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

Window with 3 buttons: one starting in the down state, one disabled and one checked & toggleable. Window with 3 buttons: one starting in the down state, one disabled and one checked & toggleable.

First, note that these three buttons have different tones of gray. These different tones of gray indicate three different states. The first button is down, the second button is disabled, and the third button is checked.

If you click the first button, then it'll be released, and its text will be set to I'm Up!. The second button won't respond to your clicks or actions. The third button will alternate its status between unchecked and checked.

Exploring Other Properties of Button Objects

QPushButton has several other useful properties we can use in our GUI applications. Some of these properties with their corresponding setter and getter method include:

Property Description Access Method Setter Method
default Indicates whether the button is the default button on the containing window or not isDefault() setDefault()
flat Indicates whether the button border is raised or not isFlat() setFlat()

The default property comes in handy when you have several buttons on a window, and you need to set one of these buttons as the default window's action. For example, say we have a dialog with the Ok and Cancel buttons. In this case, the Ok button can be your default action.

The flat property is closely related to the look and feel of your app's GUI. If we set flat to True, then the button's border won't be visible.

Associating a Popup Menu to a Button

QPushButton objects can also display a menu when you click them. To set up this menu, you must have a proper popup menu beforehand. Then you can use the setMenu() method to associate the menu with the button.

Here's an example that creates a button with an attached popup menu:

python
import sys

from PyQt6.QtWidgets import (
    QApplication,
    QMenu,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="Menu!", parent=self)

        self.menu = QMenu(self)
        self.menu.addAction("First Item")
        self.menu.addAction("Second Item")
        self.menu.addAction("Third Item")

        self.btnOne.setMenu(self.menu)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import (
    QApplication,
    QMenu,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="Menu!", parent=self)

        self.menu = QMenu(self)
        self.menu.addAction("First Item")
        self.menu.addAction("Second Item")
        self.menu.addAction("Third Item")

        self.btnOne.setMenu(self.menu)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create a button with an associated popup menu. To attach the menu to the target button, we use setMenu(), which turns the button into a menu button.

If you run this application, then you'll get a window that will look something like this:

Window with a single button, with attached drop-down menu. Window with a single button, with attached drop-down menu.

In some window styles, the button will show a small triangle on the right end of the button. If we click the button, then the pop-up menu will appear, allowing us to select any available menu option.

Conclusion

Push buttons are pretty useful widgets in any GUI application. Buttons can respond to the user's events and perform actions in our applications.

In this tutorial, you've learned how to create, use, and customize your button while building a GUI application.

For more, see the complete PySide6 tutorial.

Place Layout Manager in Tkinter

In this tutorial we are going to focus on the third of the layout managers in Tkinter, place, to get you on your way to building GUI applications.

If you are curious about the other layout managers, or not sure which one to use for your projects, you can check out the other tutorials:

The Place Layout Manager

The place layout manager allows for you to have more absolute control about the arrangement of your widgets. With place, you can specify the size of the widget, as well as the exact x and y coordinates to arrange it within the parent window. In many cases, this can prove to be easier when you are thinking about the layout of your GUI. But it also means that you may have to spend a little more time playing around with the x and y values.

The place manager is most useful for arranging buttons or other smaller widgets together within a larger dialog window.

A few of the parameters you can play around with are listed below.

  • in_ -- specifies the master window for the widget
  • x, y -- specifies the specific x and y values of the widget in the parent window
  • relx, rely -- horizontal and vertical offset relative to the size of the parent widget, values between 0.0 and 0.1
  • relwidth, relheight -- set height and width of widget relative to the size of the parent widget, values between 0.0 and 0.1
  • anchor -- where the widget is placed in the parent widget, specified by 'n', 's', 'e', 'w', or some combination of them. Default is 'center'

Create A Simple GUI With Place

Let's take a look at a simple example to learn how to lay out widgets using place. Below we create a program that asks the user a question and allows the user to select an option from a list, using a Listbox, before closing the window.

python
from tkinter import *

def get_selection():
    '''
    Get users selection and print to terminal.
    '''
    selection = lb_of_cities.curselection()  # function takes current selection from listbox
    print(lb_of_cities.get(selection))

root = Tk()
root.title("Place layout Example")
root.geometry("300x300+50+100")  # width x length + x + y

# create label in window
text = Label(root, text="Which of the following cities would you like to travel to?", wraplength=200)
text.place(x=50, y=20)

# create listbox to hold names
lb_of_cities = Listbox(root, selectmode=BROWSE, width = 24)  # width is equal to number of characters
lb_of_cities.place(x=40, y=65)
cities = ["Beijing", "Singapore", "Tokyo", "Dubai", "New York"]

# add items to listbox
for  c  in  cities:
    lb_of_cities.insert(END, c)

# set binding on item select in listbox
# when item of listbox is selected, call the function get_selection
lb_of_cities.bind("&lt;&lt;ListboxSelect&gt;&gt;", lambda event:  get_selection())

# button to close application
end_button = Button(root, text="End", command=quit)
end_button.place(x=125, y=250)

root.mainloop()

The code above will produce the following GUI:

Tkinter GUI window created using place layout manager Tkinter GUI window created using place layout manager

The GUI application itself is very simple consisting of a label, a listbox, and a button. The example above only shows how to use absolute positioning by setting the x and y values. In lines 15, 19 and 32, each widget is arranged in the window by specifying these values.

Summary

Today's post covers some of the fundamentals of using place for layout management in Tkinter to create a GUI application. While this method may be a bit more time-consuming to use, it is very useful when you want more control over the exact location of your widgets.

Grantlee version 5.3.1 now available

I’ve just made a new 5.3.1 release of Grantlee. The 5.3.0 release had some build issues with Qt 6 which should now be resolved with version 5.3.1.

Unlike previous releases, this release will not appear on http://www.grantlee.org/downloads/. I’ll be turning off grantlee.org soon. All previous releases have already been uploaded to https://github.com/steveire/grantlee/releases.

The continuation of Grantlee for Qt 6 is happening as KTextTemplate so as not to be constrained by my lack of availability. I’ll only make new Grantlee patch releases as needed to fix any issues that come up in the meantime.

Many thanks to the KDE community for taking up stewardship and ownership of this library!

Grid Layout Manager in Tkinter

In this tutorial you are going to take a more detailed look at understanding how to arrange widgets with Tkinter's grid layout manager.

In previous tutorials we talked about designing your UI layout. If you are interested, check out:

So for this post, I really want to just jump right in and help you to see more examples of designing UIs with Tkinter.

Grid Layout Manager

I personally think that using the grid layout manager is the easiest manager out of the three managers that Tkinter offers. The reason is because it works similar to a matrix, with rows and columns. The upper left-most corner has row value 0 and column value 0. So moving around the grid and arranging widgets is quite simple. If you want to place widgets in a horizontal row, then the row value stays the same and the column value increases by 1 for each widget. It is similar if you want to move down a column, with the column value staying the same and the row value increasing. Check out the image below for a visual example.

Graph showing how the grid layout manager works in Tkinter. How the grid layout manager works in Tkinter.

Notice above how each widget can be positioned within "cells" by specifying their row and column values. Widgets can also span across multiple rows or columns.

Grid Parameters

Now let's look at some of the main parameters that can help you arrange widgets using the grid layout manager.

  • row, column -- the row and column values for the location of the widget. Both start from 0.
  • columnspan, rowspan -- specifies how many columns or rows a widget will be in. This is very useful to help get the spacing right for your widgets.
  • padx, pady -- the number of pixels surrounding the widget to create padding between other widgets, for horizontal or vertical padding
  • ipadx, ipady -- how many pixels to use for padding inside the widget, also for horizontal or vertical padding
  • sticky -- specifies a value of S, N, E, W, or a combination of them, e.g. NW, NE, SW, or SE. The parameter tells which side of the "cell" the widget will "stick" to . If you use W+E+N+S, then the widget will fill up the "cell". Default is to center the widget within the "cell".

Simple Example Using Grid

The following is just a simple example to see how to set up our window and use grid(). In the following GUI, we combine LabelCheckbutton, and Button widgets to survey which of the colors people like.

Example Tkinter UI using grid with Survey that asks which colors you like. Example Tkinter UI using grid with Survey that asks which colors you like.

Let's see what the code looks like:

python
from tkinter import *

root = Tk()
root.title("Practice with Grid")
root.geometry("210x180")  # set starting size of window

def display_checked():
    '''check if the checkbuttons have been toggled, and display
    a value of '1' if they are checked, '0' if not checked'''
    red = red_var.get()
    yellow = yellow_var.get()
    green = green_var.get()
    blue = blue_var.get()

    print("red: {}\nyellow:{}\ngreen: {}\nblue: {}".format(
        red, yellow, green, blue))

# Create label
label = Label(root, text="Which colors do you like below?")
label.grid(row=0)

# Create variables and checkbuttons
red_var = IntVar()
Checkbutton(root, width=10, text="red", variable=red_var, bg="red").grid(row=1)

yellow_var = IntVar()
Checkbutton(root, width=10, text="yellow", variable=yellow_var, bg="yellow").grid(row=2)

green_var = IntVar()
Checkbutton(root, width=10, text="green", variable=green_var, bg="green").grid(row=3)

blue_var = IntVar()
Checkbutton(root, width=10, text="blue", variable=blue_var, bg="blue").grid(row=4)

# Create Buttons, one to check which colors are checked,
# and another to close the window.
Button(root, text="Tally", command=display_checked).grid(row=5)
Button(root, text="End", command=root.quit).grid(row=6)

root.mainloop()

First we start off by importing Tkinter and setting up the root window in lines 1-5. The display_checked() function is used with the Checkbutton objects to see which boxes have been selected. The get() method checks if the boxes have been selected, and if so, they return the value of '1' for selected. If not, '0'. We create a Label to display our question in line 19. Notice how in line 20 grid() is used to place the label in the window. The row parameter is set to 0, but is not necessary, in this case, for the first widget. This is because the default values for grid() are row=0, column=0. Also, the column is not included parameter because we only have one column in this interface.

Next we set about creating the four Checkbutton widgets for each color from lines 23-30. And finally the last two buttons, "Tally" and "End", are created. The tally Button calls the display_checked()~ function and prints which colors you selected to the terminal. Notice how for each widget therow` value increases by 1 in order to place it in the next row and under the previous widget.

Output for Tkinter UI, shows which colors the user selected. Output of display_checked function if yellow and green are checked.

Let's take a look at another UI that is a bit more difficult.

Profile Entry UI With Grid

Whenever you create a new account it is very important to enter your personal information so that the system can keep track of you. For the next GUI, we will focus on using grid to create an interface for the user to enter some of their personal data. The following UI uses Label, Entry, and Menu widgets.

Profile Entry UI using Grid and Tkinter. Includes user image, name, gender, eye color, height and weight entry fields. Profile Entry UI using Grid and Tkinter. Includes user image, name, gender, eye color, height and weight entry fields.

Since this tutorial's focus is to help you understand how to use grid, the widgets in the above window are not completely functional. You could add more functionality by allowing the user to select an image or saving the user's information after they finish entering it.

python
from tkinter import *

root = Tk()
root.title("Profile Entry using Grid")
root.geometry("500x300")  # set starting size of window
root.maxsize(500, 300)  # width x height
root.config(bg="lightgrey")

# Profile picture
image = PhotoImage(file="profile.gif")
small_img = image.subsample(4,4)

img = Label(root, image=small_img)
img.grid(row=0, column=0, rowspan=6, padx=5, pady=5)

# Enter specific information for your profile into the following widgets
enter_info = Label(root, text="Please enter your information: ", bg="lightgrey")
enter_info.grid(row=0, column=1, columnspan=4, padx=5, pady=5)

# Name label and entry widgets
Label(root, text="Name", bg="lightgrey").grid(row=1, column=1, padx=5, pady=5, sticky=E)

name = Entry(root, bd=3)
name.grid(row=1, column=2, padx=5, pady=5)

# Gender label and dropdown widgets
gender = Menubutton(root, text="Gender")
gender.grid(row=2, column=2, padx=5, pady=5, sticky=W)
gender.menu = Menu(gender, tearoff=0)
gender["menu"] = gender.menu

# choices in gender dropdown menu
gender.menu.add_cascade(label="Male")
gender.menu.add_cascade(label="Female")
gender.menu.add_cascade(label="Other")
gender.grid()

# Eyecolor label and entry widgets
Label(root, text="Eye Color", bg="lightgrey").grid(row=3, column=1, padx=5, pady=5, sticky=E)
eyes = Entry(root, bd=3)
eyes.grid(row=3, column=2, padx=5, pady=5)

# Height and Weight labels and entry widgets
Label(root, text="Height", bg="lightgrey").grid(row=4, column=1, padx=5, pady=5, sticky=E)
Label(root, text="inches", bg="lightgrey").grid(row=4, column=3, sticky=W)

height = Entry(root, bd=3)
height.grid(row=4, column=2, padx=5, pady=5)

Label(root, text="Weight", bg="lightgrey").grid(row=5, column=1, padx=5, pady=5, sticky=E)
Label(root, text="lbs", bg="lightgrey").grid(row=5, column=3, sticky=W)

weight = Entry(root, bd=3)
weight.grid(row=5, column=2, padx=5, pady=5)

root.mainloop()

We start again by loading the Tkinter module, creating the root window, and creating a Label widget to hold the profile image of the user. Let's look at line 14 to see how to set up the image in the root window. The image is located in row=0, column=0, but it does not appear to be arranged in the top left-most corner. This is due to the next parameter, rowspan, that allows the widget to spread across multiple rows. This can help to better align widgets. Here rowspan=6 because all of the data entry widgets on the right span across six rows.

In lines 17 and 18, we create a Label widget to let the user know what to do in this window. The enter_info object is still in row=0, but notice how we have moved over one column by setting column equal to 1. This widget also spans four columns using columnspan=4.

I won't go over all the widgets, but instead leave them to you to see how you can use grid to arrange widgets.

Summary

In today's tutorial we took a more detailed look at using the grid layout manager to create UI in Tkinter. First, we looked at a few of the parameters that can help to manipulate the layout of q GUI. Then we built two GUIs to help practice the concepts of layout management with grid.