Qt is a software development framework. It’s not just a GUI library, but it’s known for its cross-platform GUI features for desktop apps, and has carved a niche in the automotive industry. The Qt ecosystem is vast with tools and modules that sound very similar but are different: Qt Widgets, QML, Qt Quick, Qt Creator, Qt Designer, Qt Design Studio. For the game launcher I made in this post, I used Qt Creator with the integrated Qt Designer, and Qt Widgets C++ module. Links to the image assets I used are at the end of this post.
Popular applications that use/used Qt include: Autodesk Maya, Substance Designer, Battle.net by Blizzard, CryEngine, AMD Radeon, Adobe Photoshop Elements, EA Origin (discontinued), VLC, TeamViewer, Zoom, Telegram, Google Earth
Let’s first answer some frequently asked questions:
Why create your own game launcher?
- You’re creating an app that you want to distribute yourself and not through gaming platforms like Steam and Epic Store. You see this in some multiplayer and live-service games. You will need to implement your own patching / updating system.
- You want full control and customizability. You want to create a desktop app that not only launches the game, but has other features that are not available in other launchers.
- To avoid paying fees to existing game launcher services.
Why not use Unity or Unreal to make the game launcher?
I know it’s tempting. Both can build desktop apps. You already use them, so why not? Yes you can. For me, however, game engines are an overkill. We’re neither using the 3D rendering nor the game loop. Use the best tools for the job and keep things simple. Below is a table of the build size and RAM usage when I tested empty projects (release builds) for each:
Qt Widgets C++ | Unity 6 | Unreal Engine 5.4 | |
Build Size (Mb) | 31 | 94 | 373 |
RAM Usage (Mb) | 6 | 160 | 450 |
Why use Qt?
- It’s battle-tested and has a long history in the desktop space.
- You’re coming from Unreal Engine and already familiar with C++
- Easy to create a simple, cross-platform, high-performance GUI desktop app.
- It’s free as long as you comply with the LGPL license
Why NOT use Qt?
- The GPL and LGPL licensing have nuances that might be a deal braker for some projects (Ex. distributing a single binary executable).
- You don’t want to use C++ or Python. There are third-party bindings for other languages, but most of the resources are in C++ and Python. If you prefer C#, take a look at Avalonia UI. For Javascript, check Electron.
- It uses “non-standard C++” with classes like QString, QVector, QSharedPointer. There are other quirks related to memory management and naming schemes.
- Using Qt is not as simple as linking a set of C++ libraries. You might need to dive deep into CMake which is its own rabbit hole.
- You’re targeting only one platform that has better alternatives for that platform.
What is the LGPLv3 license?
- GNU Lesser General Public License v.3. Qt is available under both a commercial and an open-source license. Read here.
- To use Qt commercially without paying for a license, you should comply with LGPLv3.
How do I comply with LGPLv3?
- Read here.
- First, use only Qt modules covered under LGPL. See here to check each module license. The modules used in making a simple game launcher are covered: QtCore, QtGUI, QtNetwork, QtQml, QtQuick, QtWidgets, QtMultimedia
- Second, use dynamic linking when integrating the Qt library to your project. While it’s possible to use static linking, it will require some additional steps to comply with the license. So just use dynamic linking.
- Third, don’t modify the Qt library. If you do, your changes need to be under LGPL.
Should I use C++ or Python for Qt?
- Both are fine. Choose the one that suits your pipeline. I used C++ because I came from Unreal Engine, but I recommend trying out Python and see if you’re comfortable. Its package manager is friendlier than the messy C++ package management and build ecosystem. You might not need the performance gains of using C++ for a simple app like a launcher.
I will use Python. Should I use PyQt5 or PySide2?
- Read this article. In summary, use PySide2 because of the LGPL license. You can distribute your software without distributing the source code, and you don’t have to pay any fees as long you only use the modules covered in LGPL.
What’s the difference between Qt Widgets and Qt Quick / QML?
Alternatives to Qt
- Avalonia UI (C#)
- wxWidgets (C++)
- Electron (Javascript)
- GTK
Alternative Game Launchers
- PATCH Updating System – works on desktop but not on mobile
- Xsolla Game Launcher
- PatchKit – very good but very expensive
- Game Launcher Creator – does not work on Mac OSX and Linux
Getting Started with Qt Widgets
It takes time to learn the framework. Check out the documentation, the wiki, the forum, and watch tutorials online. Coming from Unity or Unreal Engine, you are probably used to dragging and dropping UI widgets in the hierarchy, overlaying them, and toggling the visibility of the parent widgets. Qt Designer has some quirks that you might find frustrating at first. It’s easy to use, you just need to think differently:
- The default widgets look like they came from the early 2000s. The QTabWidget for example, has useful functionalities, but is painful to look at. You will spend a lot of time styling your widgets or creating your own, so study the Stylesheet examples and search for “Qt Modern UI” in Google.
- It is not a good idea to have a lot overlapping widgets because dragging and dropping can cause them to change parents, and there is no “eye” or “lock” button to prevent you from accidentally selecting and moving widgets. Use QStackedWidget accordingly.
- There is no setting an icon image on hover by default. You’ll have to implement that yourself.
- In Qt 6, QVideoWidget always render on top of everything. Use QML as an alternative if you want to put a widget in front of a video. Read here.
- Get familiar with signals and slots.
- windeployqt.exe sometimes fail to copy the necessary .dll files when deploying a build. Follow this guide to fix it. Use batch commands to automate the process.
- I have not experienced this myself, but some developers have encountered bugs while developing apps with Qt [1] [2].
I used QuaZip and followed this guide because I was using C++ and wanted to extract .zip files. You may or may not need this.
Custom Widgets
Code
Below are snippets of code I used.
CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(GameLauncher VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt6 REQUIRED COMPONENTS MultimediaWidgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
find_package(Qt6 REQUIRED COMPONENTS Network)
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/zlib-1.3.1/install/include")
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/quazip-1.4/install/include/QuaZip-Qt6-1.4/quazip")
link_directories("${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/zlib-1.3.1/install/lib")
link_directories("${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/quazip-1.4/install/lib")
set(PROJECT_SOURCES
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(GameLauncher
MANUAL_FINALIZATION
${PROJECT_SOURCES}
filedownloader.h filedownloader.cpp
resources.qrc
ButtonHoverWatcher.h
ButtonHoverWatcher.cpp
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET GameLauncher APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
add_library(GameLauncher SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(GameLauncher
${PROJECT_SOURCES}
)
endif()
endif()
target_link_libraries(GameLauncher PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
target_link_libraries(GameLauncher PRIVATE Qt6::Network)
target_link_libraries(GameLauncher PRIVATE Qt${QT_VERSION_MAJOR}::Widgets
zlib
quazip1-qt6
)
target_link_libraries(GameLauncher PRIVATE Qt6::MultimediaWidgets)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.GameLauncher)
endif()
set_target_properties(GameLauncher PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
include(GNUInstallDirs)
install(TARGETS GameLauncher
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(GameLauncher)
endif()
File Downloader
// filedownloader.h
#ifndef FILEDOWNLOADER_H
#define FILEDOWNLOADER_H
#include <QObject>
class FileDownloader : public QObject
{
Q_OBJECT
public:
explicit FileDownloader(QObject *parent = nullptr);
void StartDownload();
signals:
void onDownloadProgress(int);
void onDownloadPause();
void OnDownloadContinue();
void onDownloadFinished();
void onDownloadFailed();
};
#endif // FILEDOWNLOADER_H
// filedownloader.cpp
#include "filedownloader.h"
#include <QtNetwork>
FileDownloader::FileDownloader(QObject *parent)
: QObject{parent}
{}
void FileDownloader::StartDownload(){
QNetworkAccessManager manager;
QUrl url("<INSERT_YOUR_DOWNLOAD_URL>");
QString fileName = url.fileName();
QNetworkReply *reply = manager.get(QNetworkRequest(url));
QEventLoop loop;
auto file = std::make_shared<QFile>(fileName);
if (!file->open(QIODevice::WriteOnly)) {
qDebug() << "Could not save file";
emit onDownloadFailed();
}
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesReceived, qint64 bytesTotal) {
int percentage = static_cast<int>(bytesReceived * 100 / bytesTotal);
emit this->onDownloadProgress(percentage);
qDebug() << "Download progress:" << bytesReceived << "of" << bytesTotal << "(" << percentage << "%)";
});
QObject::connect(reply, &QNetworkReply::readyRead, reply, [file, reply]() {
file->write(reply->readAll());
});
loop.exec();
if(reply->error()) {
qDebug() << "Download failed:" << reply->errorString();
emit onDownloadFailed();
} else {
qDebug() << "Location:" << QDir::currentPath();
file->close();
emit onDownloadFinished();
}
reply->deleteLater();
}
// mainwindow.cpp
// Example of how to bind a progress bar and a function to the downloader events.
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "filedownloader.h"
void MainWindow::on_pushButton_Download_clicked()
{
FileDownloader *downloader = new FileDownloader(this);
connect(downloader, &FileDownloader::onDownloadProgress, ui->progressBar, &QProgressBar::setValue);
connect(downloader, &FileDownloader::onDownloadFinished, downloader, &FileDownloader::deleteLater);
connect(downloader, &FileDownloader::onDownloadFinished, this, &MainWindow::onFinishDownload);
ui->pushButton_Download->setText("Downloading...");
ui->pushButton_Download->setEnabled(false);
ui->progressBar->show();
downloader->StartDownload();
}
void MainWindow::onFinishDownload()
{
ui->pushButton_Download->setText("Start Game");
ui->pushButton_Download->setEnabled(true);
ui->progressBar->hide();
}
Extract .zip file
// Example implementation after linking QuaZip
#include <JlCompress.h>
void MainWindow::on_pushButton_Extract_clicked()
{
QString outputDir = QDir::currentPath();
QString zipFilePath = outputDir + "/<INSERT_NAME_OF_ZIP_FILE>";
qDebug() << "Location:" << zipFilePath;
if (!QFile::exists(zipFilePath)) {
qDebug() << "Error: ZIP file does not exist at path:" << zipFilePath;
return;
}
JlCompress::extractDir(zipFilePath, outputDir);
}
Widget fade-in effect
Reference: https://forum.qt.io/topic/59453/solved-widget-fade-in-effect-possible
#include <QPropertyAnimation>
void MainWindow::on_pushButton_clicked()
{
QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this);
ui->exampleWidget->setGraphicsEffect(eff);
QPropertyAnimation *a = new QPropertyAnimation(eff,"opacity");
a->setDuration(200);
a->setStartValue(0);
a->setEndValue(1);
a->setEasingCurve(QEasingCurve::InBack);
a->start(QPropertyAnimation::DeleteWhenStopped);
}
Hiding the title bar
// mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowFlags(Qt::WindowType::FramelessWindowHint);
}
Playing a video
// mainwindow.cpp
#include <QMediaPlayer>
#include <QVideoWidget>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{
ui->setupUi(this);
player = new QMediaPlayer(this);
videoWidget = new QVideoWidget(this);
videoWidget->setGeometry(0,0,1280,720);
videoWidget->setParent(ui->widget_video);
player->setVideoOutput(videoWidget);
player->setSource(QUrl("qrc:<LOCATION_OF_VIDEO_IN_RESOURCE_FOLDER>"));
player->setLoops(QMediaPlayer::Infinite);
videoWidget->show();
player->play();
}
Opening URLs
#include <QDesktopServices>
void MainWindow::on_pushButton_OpenUrl_clicked()
{
QDesktopServices::openUrl(QUrl("<LINK>"));
}
windeployqt.exe Batch Command
This is an example of a command that you can copy and run in the location of the app’s executable to help automate the process of running windeployqt.exe
.
for %%i in (*.exe) do "C:\Qt\6.7.2\msvc2019_64\bin\windeployqt6.exe" "%%i"
pause
build-zlib.bat
cd zlib-1.3.1
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -S .. -B . -DCMAKE_INSTALL_PREFIX=../install
cmake --build . --config Release
cmake --install . --config Release
pause
build-quazip.bat
cd quazip-1.4
mkdir build
cd build
cmake -G "Visual Studio 17 2022" -S .. -B . -DCMAKE_INSTALL_PREFIX=../install -DCMAKE_PREFIX_PATH="C:\Qt\6.7.2\msvc2019_64\lib\cmake" -DZLIB_LIBRARY:FILEPATH="E:/Personal_Projects/2023/Qt Game Launcher/GameLauncher/thirdparty/zlib-1.3.1/install/lib/zlib.lib" -DZLIB_INCLUDE_DIR:PATH="E:/Personal_Projects/2023/Qt Game Launcher/GameLauncher/thirdparty/zlib-1.3.1/install/include"
cmake --build . --config Release
cmake --install . --config Release
pause
Other features to add in the future
- Binary diff. This will enable more efficient patching / updating. No need to download the entire build.
Links to the image assets I used:
References
- Games Launcher case study by Scythe Studio
- Expandable Sidebar menu In Python | Pyside6 / PyQt6 (2023)
- Current Issues With The Qt Project – From The Outside Looking In
- Sad state of cross platform GUI frameworks (2020)
- Game Launcher and Auto Updater with WPF | C#