Bindings for the Qt5 Framework using bindgen.
- Project Goals
- Installation
- User perspective
- Developer perspective
- Platform Support
- Contributing
- License
- 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)
Add the shard to your shard.yml:
dependencies:
qt5:
github: crystallabs/qt5.crThe generated bindings for the platforms CI builds (see
Platform Support) are included, so on those you only need
Qt5 and its development files: the binding's C++ part is compiled on first
use, which needs a C++ compiler and make. On other platforms, generate the
binding for the Qt5 installed on your system first, 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.)
Using your system's package manager (LLVM, Clang and CMake are only needed to generate a binding):
- 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 - macOS
brew install qt@5 bdw-gc llvm cmake pkgconf
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-gnuHomebrew's qt@5 is keg-only: put its qmake on the PATH (or set QMAKE)
and point pkg-config at it. Building the bindings also needs the SDK and
Homebrew's include and library directories:
export PATH=/opt/homebrew/opt/qt@5/bin:$PATH
export PKG_CONFIG_PATH=/opt/homebrew/opt/qt@5/lib/pkgconfig
export SDKROOT="$(xcrun --show-sdk-path)"
export CPPFLAGS=-I/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/libBuild bindgen's clang tool with Homebrew's LLVM:
cd lib/bindgen/clang && cmake -DLLVM_DIR=/opt/homebrew/opt/llvm/lib/cmake/llvm . && makeIf linking the tool fails with ld: could not parse bitcode object file, the
Xcode linker is older than Homebrew's LLVM, whose static libraries hold
bitcode. Link LLVM dynamically instead: BINDGEN_DYNAMIC=1 cmake ....
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.execBy 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).
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.
QSet<T> is an Enumerable(T) with #includes?, #<< (#add) and #delete,
and methods taking one accept any Enumerable. No Qt 5 method wrapped here
uses one, so Qt::QSet.of(String).from(["a", "b"]) builds a QSet<QString>
(also instantiated for int).
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).
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 thisConnecting a signal keeps the sender alive too, as Qt refers to it from the
connection: Qt::Timer.new.tap(&.on_timeout { puts "tick" }).start(1000) keeps
ticking.
Objects created from Crystal are kept alive until Qt deletes them, like a
parent deletes its children, or until the method handing them back without
deleting them releases them, like Qt::Layout#remove_widget or #take_at,
Qt::ListWidget#take_item, Qt::Widget#remove_action or
Qt::GraphicsScene#remove_item:
layout.remove_widget(label) # The GC may collect the label again
item = list.take_item(0) # And the item, once `item` is goneWhere no method hands the object back, like Qt::TabWidget#remove_tab(index),
pass it to Qt::BindgenHelper.release. An object Qt created itself, which
Crystal only wraps, is never kept: collecting its wrapper doesn't touch it.
page = Qt::Widget.new
tabs.add_tab(page, "Page")
tabs.remove_tab(0)
Qt::BindgenHelper.release(page)The methods are configured in config/keep_alive.yml, by keep_alive: and
release:. An object given a parent, by its constructor or #parent=, is
kept alive too, as the parent owns it: Qt::Label.new("Stays alive", window).
Setting the parent to nil lets it go, unless a method above keeps it: a
model given a parent, then set on a view, stays alive for the view. Releasing
it ends both, as a method handing an object back ends its ownership as well.
This is bindgen's Qt processor at work, for the classes deriving from
QObject, QGraphicsItem or QGraphicsLayoutItem, and for
Qt::TreeWidgetItem, Qt::ListWidgetItem and Qt::UndoCommand, whose
constructors take the tree, list, item or command owning them
(parent_arguments: in config/keep_alive.yml).
Qt objects belong to the thread that created them, and widgets to the main
thread. Crystal's default runtime since 1.21 may resume a fiber in another OS
thread after a blocking call, like a slow file write, which breaks Qt: timers
fail and creating a window deadlocks. So src/qt5/main_thread.cr keeps a
thread blocked in a call attached to its scheduler, as Crystal did before 1.21;
the fibers of the default context wait for the call instead of running
meanwhile. Don't resize the default execution context: Qt must be called from
the main thread. -Dwithout_mt disables execution contexts altogether, and
-Dqt5_allow_mt leaves them untouched.
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. Deleting a wrapped object counts, as its destructor calls into
Crystal to release it from keep_alive. A thread started from Crystal runs Qt
code only, like the event loop QThread::run starts by default. The signals
it emits are delivered in the main thread: the on_x connection methods
connect with the application object as the context of the connection, so with
the default Qt::ConnectionType::AutoConnection, a signal emitted in the main
thread runs the block at once, and one emitted in another thread queues the
block to the main event loop. The connection methods take the connection
type as their argument, after the type arguments of an overloaded signal.
Qt::ConnectionType::DirectConnection runs the block on the emitting thread,
so don't use it for a signal a Qt thread emits:
thread = Qt::Thread.new
thread.on_finished { puts "done" } # Called from the main event loop
thread.start
thread.quit
thread.wait
button.on_clicked(Qt::ConnectionType::QueuedConnection) { } # Run by the event loop, after the clickQThread 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 (see Fibers). The locks exclude Qt
threads from Qt data: mutex.lock, semaphore.acquire(2),
condition.wait(mutex, Qt::DeadlineTimer.new(100)).
A std::function argument is taken as a block (or as a Proc, when it isn't
the last argument), and Qt may keep the block for as long as it keeps the
function, like Qt::FileDialog.get_open_file_content. The functions Qt runs on
another thread, Qt::ThreadPool#start,
#try_start and Qt::Runnable.create, are left out, as that thread can't run
Crystal.
Crystal fibers run while Qt's event loop runs. Qt::Application.exec, like
Qt::EventLoop#exec, Qt::Dialog#exec or a waiting process_events, blocks
the calling fiber in Qt's loop, but Qt's loop drives Crystal's scheduler, so
spawn, sleep, Channel, sockets, HTTP::Client and the rest work in a Qt
program as in any other, and a slot may spawn work and return at once:
app = Qt::Application.new
label = Qt::Label.new
label.show
spawn do
loop do
label.text = Time.local.to_s
sleep 1.second
end
end
button.on_clicked do
spawn do # The slot returns at once, the fiber runs as Qt's loop idles
label.text = HTTP::Client.get(url).body
end
end
Qt::Application.execEverything runs in the main thread, so a fiber updates widgets directly.
Blocking the fiber that runs Qt's loop blocks Qt's loop, as in C++: a sleep
or a blocking read in a slot freezes the window; spawn instead. Fibers
that never wait, a Fiber.yield loop or two fibers passing a channel back and
forth, don't freeze it: after Qt::Fibers.slice (4 ms) of fiber time Qt's
loop gets a turn. samples/fibers.cr shows all of it.
How it works, in src/qt5/fibers.cr: Crystal's event loop is a kqueue (macOS,
BSD) or epoll (Linux) descriptor that becomes readable when a fiber's IO, timer
or wakeup from another thread is ready. Qt::Application.new (and the
Qt::CoreApplication.new and Qt::GuiApplication.new without arguments) puts
a QSocketNotifier on it, and Qt's loop runs the ready fibers; before Qt's
loop blocks, fibers made runnable meanwhile, like one spawned from a slot, get
their turn too. Qt::Fibers.pump suspends the fiber that runs Qt's loop and
resumes it when every other fiber waits (or after slice), so Qt's loop waits
for both Qt's events and Crystal's. A nested Qt loop started from a fiber, a
modal dialog say, pumps the same way and returns first, like Qt's own nesting.
An application created with argc and argv installs the integration on its
first exec, or call Qt::Fibers.install after creating it.
Crystal's kqueue and epoll event loops are needed (the defaults;
-Devloop=libevent isn't supported), with the execution context runtime (the
default) or -Dwithout_mt. -Dqt5_without_fibers turns the integration off:
fibers then wait while Qt's loop runs.
A Crystal subclass of a wrapped class overrides its virtual methods by
defining them, as samples/virtual_override.cr does; Qt calls the Crystal
method. super calls the C++ implementation of the class the Crystal class
derives from, and so does the private superclass wrapper from any method:
class Canvas < Qt::Widget
def mouse_press_event(event : Qt::MouseEvent) : Void
puts "pressed at #{event.pos.x}, #{event.pos.y}"
super # Same as `superclass.mouse_press_event(event)`
end
endAn instance of a Crystal subclass lives like any other object: the GC collects it once Crystal drops it, unless Qt owns it, as a parent does, a method keeping its argument alive was given it, or one of its signals is connected, see Object lifetime. Qt calls the overrides through the Crystal object as long as it lives.
An exception raised in an override, or in the block of a signal, can't unwind
through the Qt code calling it: it goes to Qt::BindgenHelper.on_exception,
which prints it with its backtrace to the standard error by default, and Qt
receives a zero of the result type, false for an event override. Replace
the handler to log or collect them:
Qt::BindgenHelper.on_exception = ->(exception : Exception) { Log.error(exception: exception) { "In a Qt callback" } }A Crystal subclass of a Qt::Object class declares its own signals, slots and
properties, and Qt sees them like those of a C++ class with Q_OBJECT:
bindgen builds a QMetaObject for the class (bindgen's SPEC.md §Q.4).
class Counter < Qt::Object
signal value_changed(value : Int32)
@[Qt::Slot]
def increment : Nil
@value += 1
value_changed(@value) # Emits the signal
end
qt_property value : Int32, write: :set_value, notify: :value_changed
end
counter.on_value_changed { |value| puts value } # Like a Qt signal
Qt::Object.connect(counter, "2valueChanged(int)".to_unsafe, label, "1setNum(int)".to_unsafe)
Qt::MetaObject.invoke_method(counter, "increment")
counter.property("value".to_unsafe).to(Int32)A @[Qt::Invokable] method is invokable by name without being a slot
(Q_INVOKABLE), and qt_class_info "QML.Element", "auto" adds a class info
(Q_CLASSINFO). The argument, result and property types are the numbers,
Bool, String, Bytes, Char (QChar), Qt::Variant (QVariant), the
wrapped enums (Qt::Orientation), the Qt::Object classes (as pointers,
nilable or not), the other wrapper classes by value (Qt::Color, copied
through its QMetaType), and the containers the binding instantiates, as
Array(T) or Enumerable(T) (QList<int>), Set(T) (QSet<int>) and
Hash(K, V) (QMap<QString,QVariant>); the C++ names are the camel-cased
Crystal names. A Crystal enum is an enumerator of the
class, like Q_ENUM, registered with Qt as Machine::Mode: property returns
it, unpacked by Qt::Variant#to(Machine::Mode), and set_property takes an
int, a key (Qt.variant("Slow")) or the enum; the values must fit an Int32.
A Crystal class deriving from another Crystal class with such members chains
its meta-object to the parent's. See spec/qt/meta_subclass_spec.cr.
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. Results Qt documents as never null, like
the menu bar a Qt::MainWindow creates on demand, the menu added to it, a
scroll area's viewport or a tree widget's header item, are configured as
non-nilable in config/methods.yml (bindgen's nilable: rule), which also
lets nil into pointer arguments Qt accepts null for, like
view.model = nil or edit.validator = nil:
if item = list.item(row)
puts item.text
end
menu = window.menu_bar.add_menu("File")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.
A pointer result or callback argument is wrapped in the class of the object's
dynamic type, the most derived class the binding knows. So
Qt::Object#children holds Qt::Labels and Qt::PushButtons (typed as
Qt::Object), Qt::CoreApplication.instance is a Qt::Application, and an
override of Qt::Widget#event receives a Qt::KeyEvent. #to? narrows the
static type, returning nil when the object isn't one:
if button = widget.to?(Qt::PushButton)
button.flat = true
endAn object constructed from Crystal comes back as the Crystal object which
constructed it, an instance of a Crystal subclass included, with its instance
variables: parent.children.first is the very label you made with
Qt::Label.new(parent). This holds while that object is alive, as the
reference the C++ object keeps to it is weak. An object Qt created gets a new
wrapper each time it is retrieved. Discovery and #to? use C++ RTTI, so the
binding is built with RTTI (ext/Makefile), as Qt itself is.
If you want to work on qt5.cr itself or you want to generate bindings for new
versions, run the generation yourself as described below.
These steps can be followed from a project using qt5.cr (in lib/qt5), or
from within qt5.cr itself.
Important: For this you'll also have to meet the dependencies of bindgen.
See lib/bindgen/README.md and lib/bindgen/clang/README.md.
The generated src/qt5/binding/binding_*.cr and ext/qt_binding_*.cpp of the
platforms CI builds are committed. CI regenerates them on every run, warns
when the committed ones are out of date, and uploads the fresh ones as the
binding-<platform> workflow artifact: download it and commit its two files.
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.
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!
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.
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 -- --systemOn macOS, export the variables of the macOS section first. Without
SDKROOT the parser can't find the SDK's frameworks (OpenGL/gl.h) and
silently generates a broken binding, with constructors missing and d_ptr
accessors added, which then fails to compile; without CPPFLAGS compiling it
can't find gc/gc.h.
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.
- 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
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.QStringView,QStringRefandQLatin1Stringmap toStringtoo, as views of a temporaryQStringliving for the call, and a view returned is copied, soXmlStreamReader#nameis aString; an overload taking a view next to one taking aQStringis dropped (bindgen'sshadowed_by:). The overloads taking the oldQRegExpare dropped, asQRegularExpressionis wrapped, and so areRegularExpression#matchof a view, whose match would keep it.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), keeping the value for Qt:label.set_property("font", other.property("font"))works. 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,QTextEdit::ExtraSelection,QFormLayout::TakeRowResult,QTextOption::Tab,QInputMethodEvent::Attribute,QPainter::PixmapFragment,QAbstractEventDispatcher::TimerInfo,QIconEngine::ScaledPixmapArgumentandQTextStreamManipulator, bare structs of Qt classes, are wrapped as classes with property methods:Qt::TextDocumentLayoutPaintContext,Qt::TextLayoutFormatRange,Qt::TextEditExtraSelection.new(cursor: cursor, format: format),form_layout.take_row(0).field_item,Qt::TextOptionTab.new(40.0, Qt::TextOption::TabType::RightTab),input_method_event.attributes.map(&.value),Qt::PainterPixmapFragment.create(pos, source_rect),stream << Qt.q_set_field_width(5) << 42.QSettings::registerFormatis dropped, as itsReadFuncandWriteFuncare C function pointers;Qt::Settings::Format::IniFormatand the native format need none.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, andQWidget::raiseis#raise_, as it would shadow Crystal'sraisein a widget written in Crystal (config/methods.yml).- A
QStringtaken by non-const reference or pointer is an out argument, returned with the result (bindgen's SPEC.md §2.2.5):state, input, pos = validator.validate("42", 0),validator.fixup("4 2")returns the fixed string,FileDialog.get_open_file_name(parent, caption, dir, filter)returns{file_name, selected_filter},TextStream#read_line_into{Bool, String}andLockFile#lock_info{Bool, Int64, String, String}. So is abool *ok,int *orint &a method writes through (builtin_out_argumentsinqt.yml):value, ok = locale.to_int("42"),year, month, day = date.get_date,r, g, b, a = color.get_rgb,inverse, invertible = transform.inverted,font, ok = FontDialog.get_font. A pointer to bytes or characters, likeIODevice#read(buffer, size), stays a pointer, and so do the arrays ofMatrix4x4#copy_data_toandFontMetrics#bounding_rect(out_arguments:inconfig/methods.yml). A validator or spin box written in Crystal returns the same tuple from itsvalidate.QTextStream::setStringandQBuffer::setBufferkeep the pointer instead, and are ignored. - 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. - The
QGenericMatrixtypedefs (QMatrix2x2...QMatrix4x3) areQt::Matrix2x2...Qt::Matrix4x3, soMatrix4x4#normal_matrixandQuaternion#to_rotation_matrixreturn aMatrix3x3. The free operators (+,-,*) aren't wrapped; useadd!,sub!,mul!anddiv!.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, copied into aString: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)andstream.read(Bytes)over helpers, andstream.read(Int32)orstream.read(Qt::Point)over the wrapped>>(Int32*)and>>(Point*), which take a pointer to the Crystal value:stream >> pointerof(point).stream << "text",<< bytesand<< pointare wrapped as is.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 aCborStringResult(result.status.ok?,result.data) andreadByteArrayaCborByteArrayResult;reader.read_all_stringand#read_all_byte_array(Qt 6's names) loop over the chunks in Crystal. Qt 5 declares theQDataStreamoperators ofQCborSimpleTypewithout exporting them, soext/cbor_helper.hppdefines 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_foris a template over the duration type:config/types.ymldeclares itsstd::chrono::millisecondsinstantiation (bindgen'sdeclare_methods:), somutex.try_lock_for(10.milliseconds)works;try_lock_untilisn't bound.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 are ignored, which drops the methods using them.
- The rest of the billion Qt classes of interest
- Integration for the
Qt DesignerUI designer - Integration for
Qt Linguist- Localization/Translation for your applications!
- UI test library, with adapter for
spec - Automated copy (and adaption) of the Qt documentation, for easy Crystal-specific docs
- The Qt Docs license should allow this if done correctly
| System | Status | Qt version | Notes |
|---|---|---|---|
| Ubuntu 24.04 | Works | 5.15 | CI, with LLVM 14 and 18 |
macOS (Homebrew qt@5) |
Works | 5.15 | CI |
| ArchLinux | Should work | 5.15 | Help wanted |
| Windows | Untested | ? | Help wanted |
The bindings target Qt 5.15 and hide the API it deprecates (see Deprecated Qt API), so older Qt 5 versions are untested.
- 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 :)
- Papierkorb Stefan Merettig - creator
- docelic Davor Ocelic - maintainer
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 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.
