Documentation
¶
Overview ¶
qbackend seamlessly bridges a backend Go application with QtQuick/QML for user interfaces.
This package bridges Go and QML for Go applications to implement QML frontends with nearly-seamless sharing of objects and types. It aims to be normal and intuitive from either language, with as little API or specialized code as possible.
It also allows for out-of-process UI. All frontend/backend communication is socket-based; the Go application does not use cgo or any native code. For all-in-one applications, the backend/qmlscene package provides a simple wrapper to execute QML within the Go process.
This package includes all of the backend API. In QML, the corresponding Crimson.QBackend plugin provides the objects and types exposed from the backend.
Objects ¶
In the middle of everything is QObject. When QObject is embedded in a struct, that type is "a QObject" and will be a fully functional Qt object in the frontend. The exported fields become QML properties, exported methods are callable functions, and func fields create Qt signals. Properties and parameters can contain other QObjects, structs (as JS objects), maps, arrays, and any encodable type. QObject also embeds a few useful methods for the backend implementation, such as signalling property changes.
// Go
type Demo struct {
qbackend.QObject
Steps []*Demo
}
func (d *Demo) Run() {
...
}
// QML
property var demo: Backend.topDemo
property int numSteps: demo.steps.length
onClicked: { demo.run(); demo.steps = [] }
QObjects are used by passing them (by pointer) to the frontend in properties, signals, and return values of other objects, or by registering singleton or instantiable types. They do not need to be initialized explicitly, and they will be garbage collected normally once there are no remaining references to the object from Go or QML. Generally, there is no need to treat them differently from any other type.
Data Models ¶
For large, complex, or dynamic data used in QML views, Model provides a QAbstractListModel equivalent API. An object which embeds Model, implements the ModelDataSource interface, and calls Model's methods for changes to data is usable as a model anywhere in QML.
Singletons ¶
Instances of QObject types can be registered during startup as singletons. These singleton objects are available in QML from startup as their uppercase name, like any normal QML singleton, and are never deleted. In all other ways, they behave like any other object.
// Go
type Demo struct {
qbackend.QObject
Value int
}
demo := &Demo{Value: 12345}
qb.RegisterSingleton("Demo", demo)
// QML
import Crimson.QBackend 1.0
Text {
text: "Demo value: " + Demo.value
}
To interact with the backend, there must be at least one singleton or instantiable type (below). These are the entry points of your backend API.
Instantiable Types ¶
The objects referenced so far are all created from the Go backend and given to the QML frontend. QML could call a function on an existing object to get a new object, but couldn't create anything declaratively. For this, we have instantiable types.
Any QObject type registered during startup with Connection.RegisterType becomes instantiable. These are real QML types and can be created and used declaratively like any other QML type:
// Go
type Demo struct {
qbackend.QObject
Value int
}
qb.RegisterType("Demo", &Demo{})
// QML
import Crimson.QBackend 1.0
Demo {
value: 123
onValueChanged: { ... }
}
Optional interface methods allow the QObject to initialize values and act when construction is completed or after QML destruction.
Connection ¶
Connection handles communication with the frontend and manages objects. It's used during during startup but otherwise isn't usually important.
Connection is created with a socket for communication with the frontend. Its documentation describes the sockets and corresponding behavior of the QML plugin in more detail. In many cases, wrappers like backend/qmlscene can be used to avoid dealing with sockets.
Finally, the connection is started by calling Run() or (in a loop) Process(). Be aware that any members of any initialized QObjects can be accessed during calls to Run, Process, or calls by the application to some methods of this package. RunLockable() provides a sync.Locker for exclusive execution with Process(). See those methods for details on avoiding concurrency issues.
Executing QML ¶
The choice of how to manage executing the backend and QML client is up to the application. They can be separate processes or a single Go process, they can execute together or rely on a daemon, and the client or backend could be executed first. qbackend could use more convenient tools for managing this process.
For applications that want to simply run as a Go binary and execute QML in-process, the backend/qmlscene package provides a convenient wrapper for qbackend and https://github.com/special/qgoscene. This makes it possible to set up an application with only a few lines of code.
Index ¶
- func SortModelInserted(model SortableModel, start, end int)
- type AnyQObject
- type Connection
- func (c *Connection) InitObject(obj AnyQObject) error
- func (c *Connection) InitObjectId(obj AnyQObject, id string) error
- func (c *Connection) Object(name string) AnyQObject
- func (c *Connection) Process() error
- func (c *Connection) ProcessSignal() <-chan struct{}
- func (c *Connection) RegisterSingleton(name string, object AnyQObject) error
- func (c *Connection) RegisterType(name string, template AnyQObject) error
- func (c *Connection) RegisterTypeFactory(name string, t AnyQObject, factory func() AnyQObject) error
- func (c *Connection) Run() error
- func (c *Connection) RunLockable() (sync.Locker, <-chan error)
- func (c *Connection) Started() bool
- type InvokedPromise
- type Model
- type ModelDataSource
- type ModelDataSourceRows
- type QObject
- func (o *QObject) Changed(property string)
- func (o *QObject) Connection() *Connection
- func (o *QObject) Emit(signal string, args ...interface{})
- func (o *QObject) Identifier() string
- func (o *QObject) MarshalJSON() ([]byte, error)
- func (o *QObject) Referenced() bool
- func (o *QObject) ResetProperties()
- type QObjectHasInit
- type QObjectHasStatus
- type SortableModel
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SortModelInserted ¶
func SortModelInserted(model SortableModel, start, end int)
Sort newly inserted rows [start:end] into positions <= start and emit signals
Types ¶
type AnyQObject ¶
type AnyQObject interface {
// contains filtered or unexported methods
}
AnyQObject is an interface to receive any type usable as a QObject
type Connection ¶
type Connection struct {
// contains filtered or unexported fields
}
func NewConnection ¶
func NewConnection(data io.ReadWriteCloser) *Connection
NewConnection creates a new connection from an open stream. To use the connection, types must be registered, and Run() or Process() must be called to start processing data.
func NewConnectionSplit ¶
func NewConnectionSplit(in io.ReadCloser, out io.WriteCloser) *Connection
NewSplitConnection is equivalent to Connection, except that it uses spearate streams for reading and writing. This is useful for certain kinds of pipe or when using stdin and stdout.
func (*Connection) InitObject ¶
func (c *Connection) InitObject(obj AnyQObject) error
InitObject explicitly initializes a QObject, assigning an identifier and setting up signal functions.
It's not necessary to InitObject manually. Objects are automatically initialized as they are encountered in properties and parameters.
InitObject can be useful to guarantee that a QObject and its signals are non-nil and can be called, even when it may have not yet been sent to the client.
func (*Connection) InitObjectId ¶
func (c *Connection) InitObjectId(obj AnyQObject, id string) error
InitObjectId is equvialent to InitObject, but takes an identifier for the object. Nothing is changed if the object has already been initialized.
In some cases, it's useful to look up an object by a known/composed name, because holding a reference to that object would prevent garbage collection. This is particularly true when writing wrapper types where the object is uniquely wrapping another non-QObject type.
func (*Connection) Object ¶
func (c *Connection) Object(name string) AnyQObject
Object returns a registered QObject by its identifier
func (*Connection) Process ¶
func (c *Connection) Process() error
Process handles any pending messages on the connection, but does not block to wait for new messages. ProcessSignal signals when there are messages to process.
Application data (objects and their fields) is never accessed except during calls to Process() or other qbackend methods. By controlling calls to Process, applications can avoid concurrency issues with object data.
Process returns nil when no messages are pending. All errors are fatal for the connection.
func (*Connection) ProcessSignal ¶
func (c *Connection) ProcessSignal() <-chan struct{}
func (*Connection) RegisterSingleton ¶
func (c *Connection) RegisterSingleton(name string, object AnyQObject) error
RegisterSingleton makes an object available globally in QML as a singleton. These objects always exist and can be used by their name anywhere within QML.
Singletons must be registered before the connection is started, and the name must start with an uppercase letter, which is how they appear within QML, and must not conflict with any other type.
A singleton object's ID is its name. It can be found with Connection.Object().
func (*Connection) RegisterType ¶
func (c *Connection) RegisterType(name string, template AnyQObject) error
RegisterType registers a type to be creatable from QML. Instances of these types can be created, assigned properties, and used declaratively like any other QML type.
It is _not_ necessary to register QObject types that are simply given to QML. This registration allows new instances of a type to be created within QML in declarative syntax.
New instances are copied from the template object, including its values. This means you can set fields for the instantiated type that are different from the zero value. This is equivalent to a Go value assignment; it does not perform a deep copy.
RegisterType must be called before the connection starts (calling Process or Run). There is a limit of 10 registered types; if this isn't enough, it could be increased.
The methods described in QObjectHasInit and QObjectHasStatus are particularly useful for instantiated types to handle object creation and destruction.
Instantiated objects are normal objects in every way, including for garbage collection.
func (*Connection) RegisterTypeFactory ¶
func (c *Connection) RegisterTypeFactory(name string, t AnyQObject, factory func() AnyQObject) error
RegisterTypeFactory registers a type to be creatable from QML. Instances of these types can be created, assigned properties, and used declaratively like any other QML type.
It is _not_ necessary to register QObject types that are simply given to QML. This registration allows new instances of a type to be created within QML in declarative syntax.
The factory function is called to create a new instance. The QObject 't' should be a pointer to the zero value of the type (&Type{}). This is used only for type definition, and its value has no meaning. The factory function must always return this same type.
RegisterType must be called before the connection starts (calling Process or Run). There is a limit of 10 registered types; if this isn't enough, it could be increased.
The methods described in QObjectHasInit and QObjectHasStatus are particularly useful for instantiated types to handle object creation and destruction.
Instantiated objects are normal objects in every way, including for garbage collection.
func (*Connection) Run ¶
func (c *Connection) Run() error
Run processes messages until the connection is closed. Be aware that when using Run, any data exposed in objects could be accessed by the connection at any time. For better control over concurrency, see Process.
Run is equivalent to a loop of Process and ProcessSignal.
func (*Connection) RunLockable ¶
func (c *Connection) RunLockable() (sync.Locker, <-chan error)
RunLockable executes Run() in a separate goroutine and returns a sync.Locker, which can be used for mutually exclusive execution with Process(). That is, locking guarantees that Process() is not and will not run until unlocked.
As noted on Process(), application data is only accessed during calls to Process or other qbackend methods. Objects can be safely modified while holding this lock. Other methods of Connection and QObject can be used while holding the lock. However, like all other Go locks, this lock is not recursive. Attempting to lock from within a call to Process will deadlock.
RunLockable also returns a channel, which will receive one error value and close when the connection is closed.
func (*Connection) Started ¶
func (c *Connection) Started() bool
Started returns true if the connection has been started by a call to Run() or Process()
type InvokedPromise ¶
type InvokedPromise struct {
// contains filtered or unexported fields
}
func NewInvokedPromise ¶
func NewInvokedPromise() *InvokedPromise
func (*InvokedPromise) Reject ¶
func (p *InvokedPromise) Reject(err error)
func (*InvokedPromise) Resolve ¶
func (p *InvokedPromise) Resolve(values ...interface{})
type Model ¶
type Model struct {
QObject
// ModelAPI is an internal object for the model data API
ModelAPI *modelAPI `json:"_qb_model"`
}
Model is embedded in another type instead of QObject to create a data model, represented as a QAbstractItemModel to the client.
To be a model, a type must embed Model and must implement the ModelDataSource interface. No other special initialization is necessary.
When data changes, you must call Model's methods to notify the client of the change.
func (*Model) InitObject ¶
func (m *Model) InitObject()
type ModelDataSource ¶
Types embedding Model must implement ModelDataSource to provide data
type ModelDataSourceRows ¶
type ModelDataSourceRows interface {
ModelDataSource
Rows() []interface{}
}
Types embedding Model _may_ implement ModelDataSourceRows to provide a list of all rows more efficiently.
If the implementation of Rows() requires copying to a new slice, it may be more efficient to not implement this function.
type QObject ¶
type QObject struct {
// contains filtered or unexported fields
}
The QObject interface is embedded in a struct to make that object appear as a fully interactive object on the QML frontend. These objects are equivalent to a Qt QObject with full support for properties, methods, and signals.
type Thing struct {
backend.QObject
Strings []string
Signal func(int) `qbackend:"value"`
}
func (t *Thing) Join(otherThing *Thing) (string, error) {
if otherThing == nil {
return "", errors.New("missing other Thing")
}
allStrings := append(t.Strings, otherThing.Strings...)
return strings.Join(allStrings, " "), nil
}
Properties ¶
Exported fields are properties of the object. To match QML's syntax, the first letter of the property name is always made lowercase. Properties can be renamed using the `json:"xxx"` field tag. Fields tagged with `qbackend:"-"` or `json:"-"` are ignored.
Properties are read-only by default. If a method named "setProp" exists and takes one parameter of the correct type, the property "prop" will be writable in QML by automatically calling that set method.
Properties have change signals (e.g. "propChanged") automatically. When the value of a field changes, call QObject.Changed() with the property name to update the value and emit the change signal.
Signals ¶
Signals are defined by exported fields with a func type and a tag with the names of its parameters:
ThingHappened func(string, string) `qbackend:"what,how"`
As usual, the first letter of the signal name is lowercase within QML. The parameters must be explicitly named; these are the names of variables within a QML signal handler. Signals are emitted asynchronously.
During QObject initialization (see below), signal fields are assigned a function to emit the signal. After initialization, signals can simply be called like methods. Take care when emitting signals from objects that may not have been used yet, because the signals may be nil. Custom functions can be assigned to the field instead; they will not be replaced during initialization, and QObject.Emit() can be used to emit the signal directly.
Methods ¶
Exported methods of the struct can be called as methods on the object. To match QML syntax, the first letter of the method name will be lowercase. Any serializable (see below) types can be used in parameters and return values, including other QObjects.
Calls to Go methods from QML are asynchronous. In QML, all Go backend methods return a javascript Promise object. That promise is resolved with any return values from the backend or rejected in case of errors. There is no way to call methods synchronously.
If the Go method returns multiple values, they are passed as an array when the Promise is resolved. If the last (or only) return type is `error` and is not nil, the Promise is rejected with that error. Nil errors are not included in the return values.
Using the Thing example above from QML:
Thing {
id: obj
strings: [ "one", "two" ]
}
Thing {
id: obj2
strings: [ "three" ]
}
onClicked: {
obj.join(obj2).then(
result => {
console.log("joined string:", result)
},
error => {
console.warn("join failed:", error)
}
)
}
Serializable Types ¶
Properties and parameters can contain any type serializable as JSON, pointers to any QObject type, and any of these types within interfaces, structs, maps, slices, and arrays. These are mapped as expected to QML and Javascript types, with non-QObject structs as static JS objects. QObjects are mapped to the same object instance.
As an implementation detail, serialization uses MarshalJSON for all types other than QObjects. QObject implements MarshalJSON to return a light reference to the object without any values; serialization is not recursive through QObjects. Serialization of the properties of a QObject happens internally. These details may change.
Initialization ¶
QObjects usually don't need explicit initialization. When a QObject is encountered in the properties or parameters of another object, it's initialized automatically. Initialization assigns a unique object ID and sets handlers on any nil signal fields (so they can be called directly). It's safe to call QObject's methods on an uninitialized object; they generally have no effect.
Objects can be initialized immediately with Connection. A custom object ID can be set with Connection.InitObjectId(). This can be useful when wrapping an external Go type with a QObject type, because keeping a list of pointers would prevent garbage collection. The QObject can be found by ID with Connection.Object() if it still exists.
Garbage Collection ¶
QObject types are garbage collected the same as any other type in Go. Once there are no references to an object from QML or within the properties of another referenced QObject, pointers within qbackend will be released and normal garbage collection takes place. There is no need for to handle these differently.
Specifically, the frontend keeps a reference on any object it does or could use, so objects can never disappear from under it. During serialization objects keep count of references in the properties of other objects, which tracks objects that are available to the frontend which it may have not requested yet. If both of these are unreferenced, a grace period of a few seconds handles object references that may be "in flight" as parameters and debounces.
At the end of that period, there's no valid way for the object to be used without first appearing in the serialization of other properties or parameters. The unreferenced object is "deactivated", which removes any pointers held by qbackend to allow garbage collection, but does not clear the object's unique ID.
If a deactivated object is used again, the object initialization scan reactivates it under the same ID and it can be used as if nothing had changed.
Instantiable Types ¶
QObject types registered through Connection.RegisterType() can be created from QML declaratively, like any other native type. See that method and the package documentation for details.
func (*QObject) Changed ¶
Changed updates the value of a property on the client, and sends the changed signal. Changed should be used instead of emitting the signal directly; it also handles value updates.
func (*QObject) Connection ¶
func (o *QObject) Connection() *Connection
Connection returns the connection associated with this object.
func (*QObject) Emit ¶
Emit emits the named signal asynchronously. The signal must be defined within the object and parameters must match exactly.
func (*QObject) Identifier ¶
Identifier is unique for each object. Objects can be found by their identifier from the Connection. The identifier is randomly assigned, unless it was initialized explicitly with Connection.InitObjectId.
func (*QObject) MarshalJSON ¶
Unfortunately, even though this method is embedded onto the object type, it can't be used to marshal the object type. The QObject field is not explicitly initialized; it's meant to initialize automatically when an object is encountered. That isn't possible to do when this MarshalJSON method is called, even if it were embedded as a struct instead of an interface.
Even if this object were guaranteed to have been initialized, QObjects do not marshal recursively, and there would be no way to prevent this within MarshalJSON.
Instead, marshalObject handles the correct marshaling of values for object types, and this function returns the typeinfo that is appropriate when an object is referenced from another object.
func (*QObject) Referenced ¶
Referenced returns true when there is a client-side reference to this object. When false, all signals are ignored and the object will not be encoded.
func (*QObject) ResetProperties ¶
func (o *QObject) ResetProperties()
ResetProperties is effectively identical to emitting the Changed signal for all properties of the object.
type QObjectHasInit ¶
type QObjectHasInit interface {
InitObject()
}
If a QObject type implements QObjectHasInit, the InitObject function will be called immediately after QObject is initialized. This can be used to initialize fields automatically at the right time, or even as a form of constructor.
type QObjectHasStatus ¶
type QObjectHasStatus interface {
ComponentComplete()
ComponentDestruction()
}
When instantiable QObjects are created from QML, these methods will be called on construction (after all initial properties are set) and destruction respectively if they are implemented. It is not necessary to implement both methods.
These methods are never called for objects that aren't created from QML.
type SortableModel ¶
type SortableModel interface {
// Inherently implemented by models
ModelDataSource
Inserted(start, count int)
// RowLess is a less function, equivalent to the sort package
RowLess(i, j int) bool
// RowMove should move row 'src' to index 'dst', without emitting signals
RowMove(src, dst int)
}
SortableModel can be implemented by models to use the SortModel functions, which handle logic to keep models sorted during insertions and changes.