Our text editor looks simple and we need to decorate it. Using QML, we can declare transitions and animate our text editor. Our menu bar is occupying one-third of the screen and it would be nice to have it only appear when we want it.
We can add a drawer interface, that will contract or expand the menu bar when clicked. In our implementation, we have a thin rectangle that responds to mouse clicks. The drawer, as well as the application, has two sates: the "drawer is open" state and the "drawer is closed" state. The drawer item is a strip of rectangle with a small height. There is a nested Image element declaring that an arrow icon will be centered inside the drawer. The drawer assigns a state to the whole application, with the identifier screen, whenever a user clicks the mouse area.
Rectangle{ id:drawer height:15 Image{ id: arrowIcon source: "images/arrow.png" anchors.horizontalCenter: parent.horizontalCenter } MouseArea{ id: drawerMouseArea anchors.fill:parent onClicked:{ if (screen.state == "DRAWER_CLOSED"){ screen.state = "DRAWER_OPEN" } else if (screen.state == "DRAWER_OPEN"){ screen.state = "DRAWER_CLOSED" } } ... } }
A state is simply a collection of configurations and it is declared in a State element. A list of states can be listed and bound to the states property. In our application, the two states are called DRAWER_CLOSED and DRAWER_OPEN. Item configurations are declared in PropertyChanges elements. In the DRAWER_OPEN state, there are four items that will receive property changes. The first target, menuBar, will change its y property to 0. Similarly, the textArea will lower to a new position when the state is DRAWER_OPEN. The textArea, the drawer, and the drawer's icon will undergo property changes to meet the current state.
states:[ State{ name: "DRAWER_OPEN" PropertyChanges { target: menuBar; y:0} PropertyChanges { target: textArea; y: partition + drawer.height} PropertyChanges { target: drawer; y: partition} PropertyChanges { target: arrowIcon; rotation: 180} }, State{ name: "DRAWER_CLOSED" PropertyChanges { target: menuBar; y:-partition} PropertyChanges { target: textArea; y: drawer.height; height: screen.height - drawer.height} PropertyChanges { target: drawer; y: 0} PropertyChanges { target: arrowIcon; rotation: 0} } ]
State changes are abrupt and needs smoother transitions. Transitions between states are defined using the Transition element, which can then bind to the item's transitions property. Our text editor has a state transition whenever the state changes to either DRAWER_OPEN or DRAWER_CLOSED. Importantly, the transition needs a from and a to state but for our transitions, we can use the wild card * symbol to denote that the transition applies to all state changes.
During transitions, we can assign animations to the property changes. Our menuBar switches position from y:0 to y:-partition and we can animate this transition using the NumberAnimation element. We declare that the targets' properties will animate for a certain duration of time and using a certain easing curve. An easing curve controls the animation rates and interpolation behavior during state transitions. The easing curve we chose is �{PropertyAnimation::easing.type}{Easing.OutQuint}, which slows the movement near the end of the animation. Pleae read QML's Animation article.
transitions: [ Transition{ to: "*" NumberAnimation { target: textArea; properties: "y, height"; duration: 100; easing.type: Easing.OutQuint } NumberAnimation { target: menuBar; properties: "y"; duration: 100;easing.type: Easing.OutQuint } NumberAnimation { target: drawer; properties: "y"; duration: 100;easing.type: Easing.OutQuint } } ]
Another way of animating property changes is by declaring a Behavior element. A transition only works during state changes and Behavior can set an animation for a general property change. In the text editor, the arrow has a NumberAnimation animating its rotation property whenever the property changes.
In TextEditor.qml: Behavior{ NumberAnimation{property: "rotation";easing.type: Easing.OutExpo } }
Going back to our components with knowledge of states and animations, we can improve the appearances of the components. In Button.qml, we can add color and scale property changes when the button is clicked. Color types are animated using ColorAnimation and numbers are animated using NumberAnimation. The on propertyName syntax displayed below is helpful when targeting a single property.
In Button.qml: ... color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor Behavior on color { ColorAnimation{ duration: 55} } scale: buttonMouseArea.pressed ? 1.1 : 1.00 Behavior on scale { NumberAnimation{ duration: 55} }
Additionally, we can enhance the appearances of our QML components by adding color effects such as gradients and opacity effects. Declaring a Gradient element will override the color property of the element. You may declare a color in the gradient using the GradientStop element. The gradient is positioned using a scale, between 0.0 and 1.0.
In MenuBar.qml gradient: Gradient { GradientStop { position: 0.0; color: "#8C8F8C" } GradientStop { position: 0.17; color: "#6A6D6A" } GradientStop { position: 0.98;color: "#3F3F3F" } GradientStop { position: 1.0; color: "#0e1B20" } }
This gradient is used by the menu bar to display a gradient simulating depth. The first color starts at 0.0 and the last color is at 1.0.
We are finished building the user interface of a very simple text editor. Going forward, the user interface is complete, and we can implement the application logic using regular Qt and C++. QML works nicely as a prototyping tool, separating the application logic away from the UI design.
[Missing image qml-texteditor4_texteditor.png]
Now that we have our text editor layout, we may now implement the text editor functionalities in C++. Using QML with C++ enables us to create our application logic using Qt. We can create a QML context in a C++ application using the Qt's Declarative classes and display the QML elements using a Graphics Scene. Alternatively, we can export our C++ code into a plugin that the qmlviewer tool can read. For our application, we shall implement the load and save functions in C++ and export it as a plugin. This way, we only need to load the QML file directly instead of running an executable.
We will be implementing file loading and saving using Qt and C++. C++ classes and functions can be used in QML by registering them. The class also needs to be compiled as a Qt plugin and the QML file will need to know where the plugin is located.
For our application, we need to create the following items:
To build a plugin, we need to set the following in a Qt project file. First, the necessary sources, headers, and Qt modules need to be added into our project file. All the C++ code and project files are in the filedialog directory.
In cppPlugins.pro: TEMPLATE = lib CONFIG += qt plugin QT += declarative DESTDIR += ../plugins OBJECTS_DIR = tmp MOC_DIR = tmp TARGET = FileDialog HEADERS += directory.h \ file.h \ dialogPlugin.h SOURCES += directory.cpp \ file.cpp \ dialogPlugin.cpp
In particular, we compile Qt with the declarative module and configure it as a plugin, needing a lib template. We shall put the compiled plugin into the parent's plugins directory.
In dialogPlugin.h: #include <QtDeclarative/QDeclarativeExtensionPlugin> class DialogPlugin : public QDeclarativeExtensionPlugin { Q_OBJECT public: void registerTypes(const char *uri); };
Our plugin class, DialogPlugin is a subclass of QDeclarativeExtensionPlugin. We need to implement the inherited function, registerTypes. The dialogPlugin.cpp file looks like this:
DialogPlugin.cpp: #include "dialogPlugin.h" #include "directory.h" #include "file.h" #include <QtDeclarative/qdeclarative.h> void DialogPlugin::registerTypes(const char *uri){ qmlRegisterType<Directory>(uri, 1, 0, "Directory"); qmlRegisterType<File>(uri, 1, 0,"File"); } Q_EXPORT_PLUGIN2(FileDialog, DialogPlugin);
The registerTypes function registers our File and Directory classes into QML. This function needs the class name for its template, a major version number, a minor version number, and a name for our classes.
We need to export the plugin using the Q_EXPORT_PLUGIN2 macro. Note that in our dialogPlugin.h file, we have the Q_OBJECT macro at the top of our class. As well, we need to run qmake on the project file to generate the necessary meta-object code.
We can create QML elements and properties using C++ and Qt's Meta-Object System. We can implement properties using slots and signals, making Qt aware of these properties. These properties can then be used in QML.
For the text editor, we need to be able to load and save files. Typically, these features are contained in a file dialog. Fortunately, we can use QDir, QFile, and QTextStream to implement directory reading and input/output streams.
class Directory : public QObject{ Q_OBJECT Q_PROPERTY(int filesCount READ filesCount CONSTANT) Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged) Q_PROPERTY(QString fileContent READ fileContent WRITE setFileContent NOTIFY fileContentChanged) Q_PROPERTY(QDeclarativeListProperty<File> files READ files CONSTANT ) ...
The Directory class uses Qt's Meta-Object System to register properties it needs to accomplish file handling. The Directory class is exported as a plugin and is useable in QML as the Directory element. Each of the listed properties using the Q_PROPERTY macro is a QML property.
The Q_PROPERTY declares a property as well as its read and write functions into Qt's Meta-Object System. For example, the filename property, of type QString, is readable using the filename() function and writable using the function setFilename(). Additionally, there is a signal associated to the filename property called filenameChanged(), which is emitted whenever the property changes. The read and write functions are declared as public in the header file.
Similarly, we have the other properties declared according to their uses. The filesCount property indicates the number of files in a directory. The filename property is set to the currently selected file's name and the loaded/saved file content is stored in fileContent property.
Q_PROPERTY(QDeclarativeListProperty<File> files READ files CONSTANT )
The files list property is a list of all the filtered files in a directory. The Directory class is implemented to filter out invalid text files; only files with a .txt extension are valid. Further, QLists can be used in QML files by declaring them as a QDeclarativeListProperty in C++. The templated object needs to inherit from a QObject, therefore, the File class must also inherit from QObject. In the Directory class, the list of File objects is stored in a QList called m_fileList.
class File : public QObject{ Q_OBJECT Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) ... };
The properties can then be used in QML as part of the Directory element's properties. Note that we do not have to create an identifier id property in our C++ code.
Directory{ id: directory filesCount filename fileContent files files[0].name }
Because QML uses Javascript's syntax and structure, we can iterate through the list of files and retrieve its properties. To retrieve the first file's name property, we can call files[0].name.
Regular C++ functions are also accessible from QML. The file loading and saving functions are implemented in C++ and declared using the Q_INVOKABLE macro. Alternatively, we can declare the functions as a slot and the functions will be accessible from QML.
In Directory.h: Q_INVOKABLE void saveFile(); Q_INVOKABLE void loadFile();
The Directory class also has to notify other objects whenever the directory contents change. This feature is performed using a signal. As previously mentioned, QML signals have a corresponding handler with their names prepended with on. The signal is called directoryChanged and it is emitted whenever there is a directory refresh. The refresh simply reloads the directory contents and updates the list of valid files in the directory. QML items can then be notified by attaching an action to the onDirectoryChanged signal handler.
The list properties need to be explored further. This is because list properties use callbacks to access and modify the list contents. The list property is of type QDeclarativeListProperty<File>. Whenever the list is accessed, the accessor function needs to return a QDeclarativeListProperty<File>. The template type, File, needs to be a QObject derivative. Further, to create the QDeclarativeListProperty, the list's accessor and modifiers need to be passed to the consructor as function pointers. The list, a QList in our case, also needs to be a list of File pointers.
The constructor of QDeclarativeListProperty constructor and the Directory implementation:
QDeclarativeListProperty ( QObject * object, void * data, AppendFunction append, CountFunction count = 0, AtFunction at = 0, ClearFunction clear = 0 ) QDeclarativeListProperty<File>( this, &m_fileList, &appendFiles, &filesSize, &fileAt, &clearFilesPtr );
The constructor passes pointers to functions that will append the list, count the list, retrieve the item using an index, and empty the list. Only the append function is mandatory. Note that the function pointers must match the definition of AppendFunction, CountFunction, AtFunction, or ClearFunction.
void appendFiles(QDeclarativeListProperty<File> * property, File * file) File* fileAt(QDeclarativeListProperty<File> * property, int index) int filesSize(QDeclarativeListProperty<File> * property) void clearFilesPtr(QDeclarativeListProperty<File> *property)
To simplify our file dialog, the Directory class filters out invalid text files, which are files that do not have a .txt extension. If a file name doesn't have the .txt extension, then it won't be seen in our file dialog. Also, the implementation makes sure that saved files have a .txt extension in the file name. Directory uses QTextStream to read the file and to output the file contents to a file.
With our Directory element, we can retrieve the files as a list, know how many text files is in the application directory, get the file's name and content as a string, and be notified whenever there are changes in the directory contents.
To build the plugin, run qmake on the cppPlugins.pro project file, then run make to build and transfer the plugin to the plugins directory.
The qmlviewer tool imports files that are in the same directory as the application. We can also create a qmldir file containing the locations of QML files we wish to import. The qmldir file can also store locations of plugins and other resources.
In qmldir: Button ./Button.qml FileDialog ./FileDialog.qml TextArea ./TextArea.qml TextEditor ./TextEditor.qml EditMenu ./EditMenu.qml plugin FileDialog plugins
The plugin we just created is called FileDialog, as indicated by the TARGET field in the project file. The compiled plugin is in the plugins directory.
Our FileMenu needs to display the FileDialog element, containing a list of the text files in a directory thus allowing the user to select the file by clicking on the list. We also need to assign the save, load, and new buttons to their respective actions. The FileMenu contains an editable text input to allow the user to type a file name using the keyboard.
The Directory element is used in the FileMenu.qml file and it notifies the FileDialog element that the directory refreshed its contents. This notification is performed in the signal handler, onDirectoryChanged.
In FileMenu.qml: Directory{ id:directory filename: textInput.text onDirectoryChanged: fileDialog.notifyRefresh() }
Keeping with the simplicity of our application, the file dialog will always be visible and will not display invalid text files, which do not have a .txt extension to their filenames.
In FileDialog.qml: signal notifyRefresh() onNotifyRefresh: dirView.model = directory.files
The FileDialog element will display the contents of a directory by reading its list property called files. The files are used as the model of a GridView element, which displays data items in a grid according to a delegate. The delegate handles the appearance of the model and our file dialog will simply create a grid with text centered in the middle. Clicking on the file name will result in the appearance of a rectangle to highlight the file name. The FileDialog is notified whenever the notifyRefresh signal is emitted, reloading the files in the directory.
In FileMenu.qml: Button{ id: newButton label: "New" onButtonClick:{ textArea.textContent = "" } } Button{ id: loadButton label: "Load" onButtonClick:{ directory.filename = textInput.text directory.loadFile() textArea.textContent = directory.fileContent } } Button{ id: saveButton label: "Save" onButtonClick:{ directory.fileContent = textArea.textContent directory.filename = textInput.text directory.saveFile() } } Button{ id: exitButton label: "Exit" onButtonClick:{ Qt.quit() } }
Our FileMenu can now connect to their respective actions. The saveButton will transfer the text from the TextEdit onto the directory's fileContent property, then copy its file name from the editable text input. Finally, the button calls the saveFile() function, saving the file. The sloadButton has a similar execution. Also, the New action will empty the contents of the TextEdit.
Further, the EditMenu buttons are connected to the TextEdit functions to copy, paste, and select all the text in the text editor.
[Missing image qml-texteditor5_filemenu.png]
[Missing image qml-texteditor5_newfile.png]
The application can function as a simple text editor, able to accept text and save the text into a file. The text editor can also load from a file and perform text manipulation.