roll your own qt quick treeview - qt developer · pdf fileroll your own qt quick treeview...
Post on 05-Mar-2018
280 Views
Preview:
TRANSCRIPT
Roll Your Own Qt Quick TreeView
– Alex MontgomeryTelltale Games
A Bit About Me● Lead Tools Developer at Telltale Games● Background in Widgets● Strong foundation and fondness for the MVC
paradigm and Qt's model / view framework● Gradually switching to QML to create dynamic,
OpenGL accelerated UIs● Needed a way to bring new and existing models
to new Qt Quick tools, so I wrote a Qt Quick TreeView
Simplifying Assumptions● All these concepts can be applied to
complex views, but this presentation will not cover:– Multiple columns
– Headers
– Models written in QML
– Advanced view actions such as drag 'n drop
– Actual C++ TreeView source code (sorry)
Bringing MVC to QML: Goals● Support models written in C++ that stem from
the QAbstractItemModel base● Take advantage of the speed and flexibility of
Qt Quick's drawing API● Be compatible with the design and philosophy
of existing QML view types for lists and tables● Design beautiful interactive UIs that don't have
to fit in the generic confines of the QWidget-based views
The State of MVC in Qt Widgets● Qt (C++) has support for representing
models in:● Lists (QListView)● Tables (QTableView)● Trees (QTreeView)
The State of MVC in Qt Quick● Qt Quick / QML has support for displaying
models in:● Lists (ListView)● Tables (GridView)● Trees (???)
The Gap: a Qt Quick TreeView
● Difficult to design a one-size-fits-all solution● Generating acceptable performance is tricky without
cutting a few corners● With a little hacking, it does exist! Kinda. (but I don't
recommend it) – Nested ListViews using DelegateModel (formerly
VisualDataModel) with a rootIndex property
Why doesn't this exist yet?!
The Goal: Fill the Gap!● Support C++ models using QModelIndex and
model roles to communicate to the view● Optimize delegate management to support
large-scale models with sorting and filtering without sacrificing performance
● Support multiple delegate types, each written in QML or Qt Quick
● A TreeView QQuickItem with the performance of C++ exported to QML
Language Divide
Model
Descendants ofQAbstractItemModel
C++C++ QMLQML
Delegates
Complex Items,Animations,
effects
TreeView
- Delegate factory - Scroll View- Layout management - Nested Grids
Model Requirements (as Always)● Standard virtual functions (derived from QAbstractItemModel),
just as used with QWidget based views– Parent() - returns a parent index of any QModelIndex
– Index() - return a child index at a given row and col
– RowCount() - Number of rows (children) of a given model index
– ColumnCount() - number of columns (always == 1 for the purposes of this talk)
● Model roles – integer IDs of variables that the view can use to query the model about the state of a particular node addressed by a QModelIndex. The data can be any type supported by QVariant, including custom types.– Qt::DisplayRole, Qt::DecorationRole, Qt::SizeHintRole, etc.
Model Requirements (QML era)● A mapping from model roles (int) to string
representations (QByteArray) ● QHash<int, QByteArray> roleNames() ● Used to insert Javascript-style vars
(QVariants) into each delegate's scope● This is a requirement for existing Qt Quick
MVC views (ListView & GridView)
Model Additions (so says I)● A few extra roles to keep delegates flexible and efficient
– Node ID● A unique identifier greatly simplifies moving delegates around when
servicing model changes like layoutChanged● Can be used in a QMap of IDs to Delegates to ease delegate updates
– Delegate path (or QQmlComponent pointer) per Node type● Gives the TreeView the path of a QML item to factory (.qml file) for
each node type
– Expanded state● Allows the delegate to show an expander control based on model state● Allows the model to control expanded state programatically without
breaking the MVC paradigm by talking directly to the view
Supporting Legacy Models● Legacy models (file system, string, standard, etc.) will not have the
aforementioned extra roles– Solution 1: Derive from the model and provide the extra roles
– Solution 2: Provide the TreeView with lambdas or other function objects that can determine id / delegate path / expanded state based on other role data from the model
– Solution 3: Work harder, not smarter● Node ID - Not strictly necessary just allows for some nice optimizations. Must
handle layoutChanged signals as mini-model resets● Delegate path - Provide only one delegate (the existing QML delegate solution)
that changes its appearance based on data from the model (other roles)● Expanded state – Store an expanded state in the view as a structure alongside
each delegate and require that each delegate have a top-level “expanded” bool property. The view can attach to the delegate's built-in “expandedChanged” signal to allow the delegate to communicate changes back
Delegates – The Face of the Data
● Wait, what's a delegate?– A per-index node, written in QML, that
represents a row or column in a tree and gets all of its info from a QModelIndex
– These are used identically to how they are used in existing QML views (ListView and GridView)
Delegates – A Sum of Their Roles● How do you write one?
– A properly designed MVC delegate can only query data from its model index
– QML delegates receive their model data from role variables inserted into their namespace
– Typically delegates create and bind properties to their role variables
– Delegates are going to be recycled for performance reasons and must not have dangling state
Delegates – Best Practices● Most TreeViews are resizable, avoid using static heights and
widths when possible – use Layouts!● QML States and the “when” property
– Delegates can have multiple States and each State can have multiple PropertyChange objects. Each state can depend on particular role variable values and efficiently change large sets of properties when the role variable is modified
● Transitions and animations– role values change instantly but Transitions can smoothly animate
sub-properties to ehance the user experience● Delegates can even take entire QML Items from their role values for
advanced view customization (to use as children or in particular layouts)
Delegates – Talking Back● Many delegates need to talk back to the model so that they
can change the state of the underlying data– Option 1: Provide a role to access a node (QObject) on the model
back-end● Pros: The Qobject node can have properties, setter methods, or
other Q_INVOKABLE methods for whatever purpose you like● Cons: The model might not use QObjects to represent its nodes
– Option 2: Provide a role to access the current model index, and a way to directly access the the model's setData() function
● Pros: works with any model (including legacy models)● Cons: Can be tricky with QSortFilterProxy filtering
Qt Quick TreeView: Overview● Written in C++, exposed as a QQuickItem to QML, e.g.
qmlRegisterType<TreeView>("mvcLib", 1, 0, "TreeView");● Composed of a Qt Quick Controls ScrollView (or a
simple Flickable) filled with nested GridLayouts of delegates
● Divided into C++ and QML parts, each responsible for different tasks:– QML takes care of bindings and attached properties
(Layout.row, Layout.col)
– C++ takes care of model communication and delegate management
A Quick TreeView
Scroll Area
Top-leveldelegate
LeafDelegate
Each delegate with children is placed in a grid with itself at the top, and its children in subsequent rows
GridLayout Structuring● GridLayout is a QML item in QtQuick.Layouts that manages size,
spacing, and general layout of children much like QWidget layouts● GridLayout is ideal for all tree types (including multi-column trees),
as it provides attached properties for its children to specify row and column
● Row and column properties can (currently) only be specified in QML (AFAIK), so it is useful to create helper functions or “derive” a QML type from GridLayout that can encapsulate this functionality
● GridLayouts can be nested to display trees of ordered items, each with children of varying types and sizes
● Each delegate for a QModelIndex that has children is given a grid of its own
Servicing Model Signals I● Tree nodes / indices can be...● Added – handle when complete
– rowsInserted()
– columnsInserted()
● Removed – handle before the items become invalid– rowsAboutToBeRemoved()
– ColumnsAboutToBeRemoved()
● Moved – handle when complete– RowsMoved()
– layoutChanged()
Making Delegate Items (C++)● QQmlComponents serve as delegate
factories to create QML items from a C++ TreeView
● QQmlContexts inject model state via role variables into the scope of QML delegate items
● Model roles define the set of data a delegate can know about its model node
Encapsulating Data with Contexts● What is a QQmlContext?
– Contexts define a set of variables that can be accessed by items in a QML scope
– All QML Items are created into a particular context (defaulting to the QQmlEngine's root context)
– Contexts are relatively quick and efficient (this is how the “id” property works in QML)
– All names in a context must be distinct
– Context properties can be set individually or using entire property sets via a “context object”
Contexts: Properties vs Objects● Context Object: a C++ object that can use properties
declared using the Q_PROPERTY macro to represent entire (static) sets of data
● Context Object Advantages:– All properties are set at once, so QML declarative bindings
do not need to be re-evaluated every time an individual property is set
– Delegate properties might be written (poorly) to depend on each other which can cause unexpected or even non-deterministic behavior. Context objects solve this by ensuring all the object's properties are initialized simultaneously
Contexts: Properties vs Objects● Context Property: an individual named value that can be
set on a QQmlContext. These can be anything QVariants support, including custom types
● Context Property Advantages– Properties don't need to be pre-defined as a Q_PROPERTY in
a QObject written in C++. QObject-style dynamic properties are supported
– When handling role changes, existing context properties can be queried and only modified when necessary
– For models whose index properties only change a few at a time, setting individual properties instead of entire sets can be cheaper
Setting Up Model Roles Vars with Contexts
● The model defines a mapping of data roles to names using virtual QHash<int, QByteArray> roleNames()
● A context is created for each delegate shown in the TreeView
● Each context contains a context property for each named role, or a context object comprising all the individual properties
● Each delegate is created by the TreeView into its own context
● Whenever an index changes role values (dataChanged signal) the context properties are adjusted and re-evaluated
Creating Delegates from Components
● QQmlComponent takes a path to a .qml file and acts as a factory that creates QML items of that type
● When a delegate needs to be created, the Treeview:– Calls the data() method on the passed QModelIndex for each
named role
– Sets up the context with variables from the QVariants returned from each data call (C++ QVariants magically convert to Javascript vars)
– Calls create() on the QQmlComponent, passing the context as a parameter which prompts the factory to instantiate a delegate
– Gives any delegates capable of having children a GridView
– Places the created delegate in its proper row/column of its parent GridView
Servicing Model Signals II● dataChanged() - one or more role variables has
changed– Query each role value using QModelIndex's data() method
● When using context properties: Query each corresponding contextProperty of the delegate's QQmlContext and set new values when necessary (only when changed)
● When using context objects: Query all role values and set them on a new Qobject which is passed to setContextObject on the delegate's context
● modelReset()– Tear down and rebuild entire tree
Optimization: Only Manage Visible Delegates
● Minimize delegate creation, destruction, and non-visible binding changes:– Ignore addRows and removeRows signals for
QmodelIndexes that are not already in the expanded hierarchy
– Check the expanded role when handling any dataChanged signal
● Newly expanded indices need delegates for their children● Newly collapsed indices need their child delegates to be
recycled
Optimization: Recycle Delegates
● Creating and destroying QML items is surprisingly expensive, even on the desktop
● Instead of destroying delegates, they can be stored in a list (or stack for temporal locality) based on their QQmlComponent's factory path
● All delegate states must be solely defined by model roles
Optimization: Minimizing role count
● Each role must be set on each delegate's context and bindings can be expensive to re-evaluate (esp. using context properties)– Keeping the named role count to a minimum
can drastically improve performance
– Objects or containers can be defined as role variables to act as holders for multiple sub-properties
Putting It all Together● Representing large hierarchical data sets in Qt Quick is
challenging without sacrificing flexibility and/or performance
● Using a C++ TreeView and model allows for maximal performance
● Using QML / Qt Quick Items as delegates allows for a better look and feel than widget-based views including OpenGL accelerated effects
● Optimizing and recycling delegates keeps run-time performance high, even when the model calls for large data changes or sorting
File System Demo● Model: Derived from standard QFileSystemModel adding extra
roles to allow for optimization● TreeView: Optimized C++ view using recyclable delegates● Delegates: Two simple delegate types: Directory and File● Roles:
– From QFileSystemModel: whatsThis, statusTip, filePermissions, toolTip, fileName, filePath, fileIcon
– For the TreeView: nodeType, nodeDelegatePath, nodeID, nodeExpanded
– Extra: readOnly
● Test: – Adding, renaming, and removing files
– Why doesn't read-only respond instantly?
Main.qml
Directory Delegate
File Delegate
MyFileSystemModel.h
MyFileSystemModel.cpp
MyFileSystemModel.cpp (cont.)
Main.cpp
Questions● Any further questions?
top related