qt5
Qt5 bindings for Crystal
Bindings for the Qt5 Framework using bindgen.
Table of Contents
- Project Goals
- Installation
- User perspective
- Developer perspective
- Platform Support
- Contributing
- License
Project Goals
- Just Works™ - Drop the dependency into a project, and use it
- Providing an API that feels native to Crystal
- Focus on the GUI components (i.e.
QtCore,QtGui,QtWidgets)
Installation
Add the shard to your shard.yml:
dependencies:
qt5:
github: crystallabs/qt5.cr
Then generate the binding for the Qt5 installed on your system, see (Re)generating the binding for the system's Qt5. This needs bindgen and its dependencies, listed below.
Your users will be required to have the Qt5 libraries on their system because this project defaults to binding to Qt libraries dynamically. (See more on this under License below.)
Additional development dependencies
Using your system's package manager:
- ArchLinux
pacman -S qt5-base gc cmake llvm clang pcre2 - Debian/Ubuntu/Devuan
apt-get install qtbase5-dev libgc-dev cmake llvm-dev clang libclang-dev libpcre2-dev libz-dev libstdc++-dev
Bindgen needs LLVM and Clang 14 or newer, CMake 3.12 or newer and Crystal 1.8 or newer.
If linking Crystal programs fails with undefined references like
__isoc23_strtol from libgc.a, your Crystal package bundles a libgc built
for a newer glibc than your system has (e.g. recent Crystal on Debian 12).
Link against the system's libgc instead:
export CRYSTAL_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu
User perspective
Sample Crystal-Qt5 code
Have a look in samples/!
A short Hello World snippet looks like this:
require "qt5"
qApp = Qt::Application.new
label = Qt::Label.new "Hello from Crystal/Qt!"
label.show
Qt::Application.exec

Locale
By default Qt sets the locale to the system locale when an application
class (like Qt::Application) is instantiated. However, this causes
stdlib Crystal functions like to_f to behave unexpectedly, e.g.,
using a different character as decimal separator (also see the discussion
of this PR).
Therefore, in contrast to the behaviour of the C++ classes of Qt,
qt5.cr resets the numeric locale to "C" after creating an
application class instance (this follows an advice from Qt
itself).
String lists
QStringList is a QList<QString>: methods taking one accept any
Enumerable(String), and methods returning one return a Qt::StringList,
which is an Indexable(String) with Qt's own methods: #filter,
#replace_in_strings, #remove_duplicates, #sort(cs), #join, #contains,
#index_of, #remove_all, #take_first and the rest of QList.
combo_box.add_items(["one", "two"])
names = Qt::Dir.new(".").entry_list(["*.cr"], Qt::Dir::Filters::Files)
names.sort(Qt::CaseSensitivity::CaseInsensitive) # In place
names.filter(".cr").to_a
#sort(cs = CaseSensitive) sorts in place. #join is Enumerable's, which
Crystal finds before the superclass holding Qt's, with the same result.
Likewise, Qt::Polygon is an Indexable(Qt::Point).
Object lifetime
Objects created from Crystal are collected by the GC once Crystal no longer
refers to them. Qt's references, like a layout's to its widgets, are invisible
to the GC. So the methods by which Qt takes ownership of an object or keeps a
pointer to it, like Qt::Layout#add_widget, Qt::ListWidget#add_item,
Qt::MainWindow#central_widget=, Qt::AbstractItemView#model= or
Qt::PushButton#menu=, keep their arguments alive:
layout.add_widget Qt::Label.new("Stays alive")
view.model = Qt::StandardItemModel.new # Stays alive too
button.menu = Qt::Menu.new # And so does this
They're kept alive until passed to Qt::BindgenHelper.release, e.g. after
taking a widget out again:
layout.remove_widget(label)
Qt::BindgenHelper.release(label)
The methods are configured in config/keep_alive.yml. Giving an object a
parent by its constructor or #parent= doesn't keep it alive: keep a
reference to it, or add it by one of these methods.
Threads
Qt::Thread, Qt::ThreadPool and the locks are bound, but a Qt thread
must not call into Crystal: the Crystal runtime and its GC only know the
threads they start, so a Crystal method run from a Qt thread corrupts the
program. A thread started from Crystal runs Qt code only, like the event loop
QThread::run starts by default, and the queued signals it emits are
delivered in the main thread:
thread = Qt::Thread.new
thread.on_finished { puts "done" } # Called from the main event loop
thread.start
thread.quit
thread.wait
QThread and QRunnable are the one place a Crystal override of a virtual
method is unsupported: they get no C++ subclass, so a Crystal run is never
called (and a Crystal Qt::Runnable subclass can't be instantiated). Use
Crystal's own Thread or fibers for Crystal work. The locks exclude Qt
threads from Qt data: mutex.lock, semaphore.acquire(2),
condition.wait(mutex, Qt::DeadlineTimer.new(100)).
Null pointers
A method returning a pointer to a Qt object returns nil when the pointer is
null, so its result is a union with Nil: Qt::Widget#parent_widget is a
Qt::Widget?, Qt::ListWidget#item a Qt::ListWidgetItem?, and so are
Qt::AbstractItemView#model, Qt::StandardItem#child or
Qt::AnimationGroup#take_animation. Check the result, or unwrap it with
not_nil! where it can't be null, like the menu bar a Qt::MainWindow creates
on demand and the menu added to it:
if item = list.item(row)
puts item.text
end
menu = window.menu_bar.not_nil!.add_menu("File").not_nil!
Only method results are nilable. A method returning a reference, like
Qt::Widget#font, always returns an object, as does a cast like
Qt::Widget#as_paint_device. The elements of a returned list, like
Qt::StandardItemModel#take_row, are the objects themselves, and so is the
argument a signal or an overridden virtual method receives: a block of
Qt::StandardItemModel#on_item_changed gets a Qt::StandardItem, and an
override of Qt::Widget#mouse_press_event takes a Qt::MouseEvent, as
declared (see samples/virtual_override.cr). This is bindgen's
nilable_results option, set in qt.yml.
Downcasts
A wrapper is of the class a method declares, so Qt::Layout#item_at returns a
Qt::LayoutItem and Qt::Object#children holds Qt::Objects. #to?
casts to a subclass, returning nil when the object isn't one:
if button = widget.to?(Qt::PushButton)
button.flat = true
end
A cast to an abstract class, like Qt::AbstractButton, wraps the object in
its Impl class. The cast is C++'s dynamic_cast, so the binding is built
with RTTI (ext/Makefile), as Qt itself is.
Developer perspective
Generating the bindings
If you want to work on qt5.cr itself or you want to generate bindings for new
versions, then as mentioned you need to use branch master and run the
generation yourself.
These steps can be followed from a project using qt5.cr, or from within
qt5.cr itself. For the latter, you can just go to lib/bindgen and do git checkout master instead of modifying shard.yml.
Important: For this you'll also have to meet the dependencies of bindgen.
See lib/bindgen/README.md and lib/bindgen/clang/README.md.
Deprecated Qt API
The bindings don't wrap deprecated Qt API. The headers are parsed and the C++
wrappers built as C++17 with QT_DISABLE_DEPRECATED_BEFORE=0x050F00 (see
qt.yml and ext/Makefile), which hides the declarations Qt deprecated up to
5.15. What Qt deprecated later is declared [[deprecated]], which bindgen's
ignore_deprecated: true drops overload by overload, so a non-deprecated
overload like QSpinBox::valueChanged(int) stays.
Building the bindings should thus print no -Wdeprecated-declarations
warnings, and CI fails if it does.
Naming scheme
As qt5.cr supports many different versions of Qt on different platforms,
generated bindings follow a naming scheme. The scheme contains 4 components
as follows:
KERNEL - LIB_C - ARCH - qtVERSION, e.g. linux-gnu-x86_64-qt5.13
Where:
KERNELis the OS kernel, e.g.linux,darwin,windowsLIB_Cis the lib C name, e.g.gnu,musl,win32ARCHis the architecture, e.g.i686,x86_64,armVERSIONis the Qt version, e.g.5.9,5.12,5.15
The naming scheme is not strictly enforced. However, it should always end with
-qtVERSION!
(Re)generating all Qt5 binding versions
To generate bindings for several Qt5 versions from their sources:
- Cd into the
qt5.crdirectory - Edit
support/generate_bindings.crand enable all versions for which you want to generate the bindings - Run
crystal support/generate_bindings.cr
The build process will automatically download, unpack, build, and generate all
configured versions of Qt5. It'll store the downloaded and unpacked Qt5 versions
to directory called download_cache/.
Subsequent invocations of that script will use these cached assets.
The first run may take a long time, and each version of Qt5 will take up about 4GB of disk space.
(Re)generating the binding for the system's Qt5
To generate the binding for the Qt5 installed on your system, without
downloading Qt sources, run from the qt5.cr directory:
crystal support/generate_bindings.cr -- --system
It asks qmake (qmake-qt5 or qmake, or $QMAKE if set) where Qt is
installed and which version it is, and names the binding after the host, e.g.
linux-gnu-x86_64-qt5.15.
(Re)generating a specific Qt5 binding version
- Use the
masterbranch ofqt5.crin yourshard.yml - Decide which version of Qt to use, and build the scheme (See above)
- Export the binding scheme:
export BINDING_PLATFORM=linux-gnu-x86_64-qt5.15 - If you're not using your system's Qt:
export QMAKE=/path/to/qmake - Run bindgen as usual:
lib/bindgen/tool.sh qt.yml --stats - Verify with
crystal spec
Type rules
config/types.yml holds the rules Qt needs beyond bindgen's built-in types,
see the comments there:
QStringListandQVariantListare aliases ofQList<QString>andQList<QVariant>, so methods take anyEnumerable(String):combo_box.add_items(["a", "b"]).QList<QString>andQListSpecialMethods<QString>, which holds theQStringListmethods, are listed as classes, so the wrapperQt::StringListhas those methods. The overloads takingQStringView,QLatin1Stringor the oldQRegExpare dropped, as the CrystalStringmaps toQStringandQRegularExpressionis wrapped.QVariantmaps toQt::Variant, which exchanges the types Qt knows in their native form, so Qt can display, sort and store them:Nil,Bool,Int32,UInt32,Int64,UInt64,Float32,Float64,Char,String,Bytes,Array(String)(QStringList),Point,PointF,Size,SizeF,RectandRectF. Any other Crystal value is wrapped as is and comes back unchanged; aQVariantof another Qt type unpacks as unknown (#to?returnsnil). SeeQt::Variant.QByteArraymaps toBytes, copied in both directions likeQString:String.new(Qt::Url.to_percent_encoding(text)),mime_data.set_data(type, bytes).QList<QByteArray>is anEnumerable(Bytes).QFile,QDir,QTimeand the like keep their names, soQt::File,Qt::DirandQt::Timeshadow the standard library's insidemodule Qt: write::Fileand::Time::Spanthere.- The typedefs of lists, like
QFileInfoListandQModelIndexList, are aliases of theirQList, sodir.entry_info_listis anEnumerable(Qt::FileInfo);QItemSelectionderives from one, soQt::ItemSelectionis anIndexable(Qt::ItemSelectionRange). - A Crystal model only implements what its C++ counterpart would:
row_count(parent)anddata(index, role)for aQt::AbstractListModel. bindgen splits the overloads a default argument can't express off the pure virtuals, calling C++ with the default. QAbstractTextDocumentLayout::PaintContextandSelection,QTextLayout::FormatRange, bare structs of Qt classes, are wrapped as classes with property methods:Qt::TextDocumentLayoutPaintContext,Qt::TextLayoutFormatRange.QTextFrame::iteratorandQTextBlock::iteratorare CrystalIterators (src/qt5/text_iterators.cr):frame.begin.each { |frame_or_block| },block.begin.map(&.text).QFontDatabaseis an instance in Qt 5:Qt::FontDatabase.new.families.Qt::TreeWidgetItemIteratoris anIterator(TreeWidgetItem):Qt::TreeWidgetItemIterator.new(tree).each { |item| ... }.QHeaderView's protectedinitialize()is#cpp_initialize, as Crystal reserves the name for constructors.QValidator::validateandfixuptake the input by non-const reference, which bindgen can't pass aString(aQStringby value) by, soext/validator_helper.hppcalls them on a copy:validator.validate("42")returns theValidator::State,#fixupthe fixed string. Validators can't be written in Crystal: as the pure virtualvalidateis dropped, bindgen skips the C++ subclass (a warning, shown bytool.sh --debug).- The
QStyleenum constants drop their prefixes:Style::PixelMetric::ButtonMargin,Style::State::Enabled. QGraphicsWidgetderives fromQGraphicsObjectandQGraphicsLayoutItem: the wrapper has the layout item methods, and#as_graphics_layout_itemcasts it forGraphicsLinearLayout#add_itemand friends.- A
QList<QPair<...>>, likeGraphicsItemAnimation#pos_list, is anEnumerableof tuples, through theQPaircontainer rule. QMapandQHashare associative containers, exchanged as aHash-likeEnumerable({K, V})with#[],#[]?,#has_key?and#keys:StandardItemModel#item_data(index)is one ofInt32toVariant,AbstractItemModel#role_namesofInt32toBytes,CalendarWidget#date_text_formatofDatetoTextCharFormat, and the setters take aHash:model.set_item_data(index, {role => Qt.variant(x)}).QVariantMapandQVariantHash(JsonObject#to_variant_map) areHash(String, Variant)-like.QVector2D,QVector3D,QVector4D,QQuaternion,QRgba64andQPainterPath::Elementare copied structures passed by value, likeQPoint, with property methods for their public fields:element.x.- A
bool *ok = nullptrargument isok : Bool*? = nil:locale.to_int("42"), ortransform.inverted(pointerof(invertible))to learn the outcome. QMatrix3x3(aQGenericMatrixinstantiation) has no mapping, which dropsMatrix4x4#normal_matrixandQuaternion#to_rotation_matrix.QGradientStopsis aQVector<QPair<qreal, QColor>>, soGradient#stopsis anEnumerable({Float64, Color})and the setter takes an array of tuples.- The QXmlStream classes return names, values and text as
QStringRef, which has no mapping (a reference into a temporaryQStringwould dangle), sosrc/qt5/xml_stream.crwraps the helpers ofext/xml_helper.hpp:reader.name,attribute.value,reader.attributes.value("id").QXmlStreamAttributesis aQVector<QXmlStreamAttribute>, soreader.attributes.map(&.name)works. QDataStreamreadsQStringandQByteArraythrough freeoperator>>, which take them by non-const reference:src/qt5/data_stream.craddsstream.read(String),stream.read(Bytes)andstream.read(Int32)over the wrapped>>(Int32*);stream << "text"and<< bytesare wrapped as is. The>>of a copied structure (QPoint &) is dropped likeQValidator::validateabove.QTouchEvent::TouchPointisQt::TouchPoint;QPointingDeviceUniqueIdis a copied structure:PointingDeviceUniqueId.from_numeric_id(42).QJsonObject#[]returns aJsonValuecopy;QJsonArray#[]aJsonValueRef, whose#assign!writes through.QJsonObject#insertreturns aJsonObjectIterator. LikewiseCborMap#insertreturns aCborMapIteratorwith#keyand#value(aCborValueRef); the pair of references the iterator dereferences to isn't default constructible, which a pair container needs, so it is ignored.QCborTagandQCborNegativeInteger,enum classes without enumerators, areUInt64:CborValue.new(Qt::CborKnownTags::Url, value).tagis32, andCborStreamWriter#append(UInt64)appends a tag, so append integers asInt64.CborValue.new(Qt::CborValue::Type::Null)replaces thenullptrconstructor.QCborStreamReader::readStringreturns aStringResult<T>bindgen doesn't instantiate, soext/cbor_helper.hppaddsreader.read_all_stringand#read_all_byte_array(Qt 6's names), andCborStreamWriter#append(String)over the unwrappedQStringViewoverload. Qt 5 declares theQDataStreamoperators ofQCborSimpleTypewithout exporting them, so the helper defines them for the binding to link.QStyleOptionTabV4is the one V-suffixed style option still a class in Qt 5 (option.tab_index); the others are deprecated typedefs of the current classes.StyleHintReturnMask#regionandStyleHintReturnVariant#variantare properties.- A Crystal
Qt::IconEnginesubclass implementspaintandclone(seespec/qt/providers_spec.cr).cloneis declaredIconEngine?, as every pointer result is; an override returningnilpasses a null pointer to C++, as for any Crystal override of a virtual method returning a pointer. - A Crystal creator derives from the abstract
Qt::ItemEditorCreatorBase(its implicit default constructor is wrapped), implementingcreate_widget(parent)andvalue_property_name(Bytes), and registers withItemEditorFactory#register_editor(Qt::MetaType::Type::Int.to_i, creator).QItemEditorCreator<T>andQStandardItemEditorCreator<T>are templates and aren't bound. QMutexisQt::Mutex, deriving fromQBasicMutex, which is listed so the methods are inherited.QRecursiveMutexderives privately fromQMutexin Qt 5 and re-exportslock,tryLockandunlockwith using-declarations, which are wrapped.#try_lock(timeout = 0)istryLock(int): bindgen drops the std-styletry_lock()andtry_acquire()aliases, whose Crystal signatures equal or overlap the originals';try_lock_forand friends are templates.QException, which only serves throwing across QtConcurrent threads, isn't bound: it would shadow::Exceptioninside the module.- The meta-object system exchanges names as
const char *, anUInt8*:String.new(meta_object.class_name),meta_object.index_of_property("text".to_unsafe).Qt::Widget. static_meta_objectis a copy of the class'sQMetaObject, which the pointer comparison ofMetaObject#inheritsdoesn't match: pass an instance'smeta_object.QMetaObject::invokeMethodandQMetaMethod::invoketakeQGenericArguments (type name andvoid *pairs) or are templates, so they are dropped;Qt::MetaObject.invoke_method(object, "click")(ext/meta_helper.hpp) invokes a method without arguments.MetaType::Typekeeps Qt's constant names,MetaType::Type::QString.QMetaObject's publicd, an anonymous struct of moc-generated tables, gets no property method, as the struct has no Crystal wrapper. QTextCodec(Qt 5 only) isQt::TextCodec:TextStream#codec=takes one,TextCodec.codec_for_name("UTF-8".to_slice)is nilable.Qt::TextEncoderandQt::TextDecoderkeep the conversion state between calls; the codec's own stateful conversions (ConverterState) aren't wrapped.- Types without a mapping yet, like
std::functioncallbacks orQSet, are ignored, which drops the methods using them.
Future things to do
- Forwarding
qHash()of wrapped types (toObject#hash) - Integration with LibEvent: Right now, Qt blocks the whole thread.
- The rest of the billion Qt classes of interest
- Everything in the to be done category
- Integration for the
Qt DesignerUI designer - Integration for
Qt Linguist- Localization/Translation for your applications!
- UI test library, with adapter for
spec- Also, actual tests - Let's catch whacko bugs right in the CI!
- Automated copy (and adaption) of the Qt documentation, for easy Crystal-specific docs
- The Qt Docs license should allow this if done correctly
Platform Support
| System | Status | Qt version | OOTB? | | ----------------- | ---------------- | ---------- | ------- | | ArchLinux | Works always | 5.12 | YES | | Ubuntu 17.04 | Works | 5.9 | YES | | Ubuntu 16.04 | Works | 5.5 | YES | | MacOS | Help wanted | ? | ? | | Windows | Help wanted | ? | ? | | Other systems | Help wanted | ? | ? |
Ready-to-use Qt versions: 5.5 to 5.12
(This list needs updating)
Contributing
- Open a new issue on the project to discuss what you're going to do and possibly receive comments
- Read bindgen's
STYLEGUIDE.mdfor some tips - Then do the rest, PR and all. You know the drill :)
Contributors
- Papierkorb Stefan Merettig - creator
License
The Qt bindings, including the generated and manually-written parts, are subject
to the MPL-2 license. You can find a copy attached of the full license text in
the LICENSE file.
This project claims no copyright on the Qt framework or of any of its
trademarks, source, or any other assets.
A note on Qt's license
A common misconception is that you have to pay for Qt to use it in closed-source applications.
This project assumes you'll link to Qt dynamically. In this case, you can use Qt free of charge including for closed-source, commercial applications under the terms of the LGPL.
You can build closed-source applications using Crystal, this shard, and Qt for free.
Note: This section is to combat this misconception, the authors of qt5.cr
are in no way responsible to check if the same terms apply in your jurisdiction.