QML is more than just a GUI
This post is participating in the competition “Smart Phones for Smart Posts” .
Friday, as you know, is the end of the work week, the best time for fun and games. And the best games are those that are exciting, in which you need to think a little, and which you can play with friends. I decided to write one of these games.
This post is not another translation or a free presentation of a variety of QML Howto and Quick Start. Rather, it is a description of the pitfalls that you may encounter when writing a real application.
When Qt Quick / QML was just announced, Nokia sounded the words that “in the future, not only the user interface will be written in Qt Quick, but all the logic of simple applications will be written in javascript, programmers will not need to write a single line of code on the pros”. The statement was even more provocative than my headline, and immediately interested me: I decided to try to write a simple game without a single line of code on the pluses.
To stir up interest, I add that:
We learn about the rest under the cut.
As a starting point, the Tsaar game was chosen - a board logic game for two players. I sincerely hope that my insidious implementation of this game will not cause the indignation of the copyright holders, but nevertheless I recommend those interested to buy it - in live form it is much more interesting than in my performance.
And we are back to development. The first thing I saw for ten minutes was the playing field (in the figure above): in all decent games it is, as you know, rectangular. After ten minutes, I said: “Aha!”, I added nodes to the picture so that a rectangular grid formed, and then I wrote a function to check if the node actually exists.
The second problem was the pictures. As I said, I hate making interfaces, and as I draw, it's better not to think at all. Generating a picture in runtime seemed to me even more stupid. I sighed, opened inkscape, and tried to accurately draw the playing field with straight lines. On the third line, I gave up, opened the resulting .svg file in vima and, through simple calculations and manipulations with macros, straightened the existing lines and finished the missing ones. In a similar fashion, six chip images were created. That is why they are so crooked and unlike the original.
I paint my existential throwings in such detail and bring in so little code to add right now: these are the biggest problems that I encountered in the process. It really is. Position the field on the form, generate and arrange chips, make homemade buttons and display status - no problem! Animate moving chips - 2 lines! Allow and prohibit the player to move various chips depending on the phase of the move - all this is done in QML so elementary that I can only offer to read the official manual with examples. By the end of the work, the js-file with all the basic logic, including empty lines and comments, occupied as many as 173 lines or 6 functions.
Oh no, perhaps I remembered one moment that amazed me and forced me to write a crutch. This moment is called:drag'n'drop. Yes, that sounds strange, but a drag and drop in a slightly less than fully graphical toolkit is done badly. It’s possible to “drag”, not an element, but MouseArea, which lies on it. The only thing we can do is to determine which buttons you can use and what restrictions we have on coordinates. It is impossible, as when working with the system, to handle the event "they threw something at me, what is it", you cannot allow an element to throw only at certain objects. You can only handle pressed and released events. Spin further as you want. And in the examples, if my memory serves me, such stupid things are generally dealt with only with all sorts of Grids and Lists, no arbitrary positioned elements for you. Apparently because of this, by the way, it is impossible to tell the element “I didn’t like the way you were thrown, return to the place”. I'm telling you,
Therefore, I had to do the following. The property of the element, obviously, is not its coordinates on the screen - x and y - but the position on the board. Coordinates are calculated based on the position. With the pressed and released events, we remember the initial position and calculate which new one we tried to throw into the element. After that, we call the function responsible for moving the element. If the function tells us that moving is not possible, we need to do with the element what? Right, return to the starting position. Watch your hands carefully:
See this assignment minus one? Yeah. The fact is that in QML, if you assign a property the value that already lies in it (oX and oY), the engine considers that the property has not changed, and does not recalculate everything that is associated with it, in our case it’s absolute coordinates on the screen. We have to assign some obviously different value, and only then the original one.
The drag-n-drop implementation itself looks like this :
And this is where all the problems really ended. One could play the game, win for white, win for black. But - on one computer. It was boring, I longed for games on the network with friends. And here comes the second iteration of development, which is much more interesting.
Where at this moment I began to think about the third problem, but so far it has not yet reached it, we will talk about the network.
As I discovered, QML has very poor and sad network support. We have Ajax requests from the javascript engine (moreover, only asynchronous), and we have XmlListModel in our examples, which is actively ragged. I would not want to believe that all QML was created solely for easy parsing of RSS feeds.
Anyway, looking ahead, I’ll say that the most obvious illustration of the poverty of working with a network in QML is the following line:
In short, when closing the game, I would like to send a message to the server that I disconnected and the session can be killed. The whole problem is that when a signal arrives, I create an asynchronous ajax request, “send” it, and then ... and then our event loop successfully continues to work and normally terminates the program - because the cause of the signal was a pressed cross in the corner window. Voila! The request will never actually reach the server. Never. But I try, I believe in the best.
In the meantime, this is all in the future, now I'm still looking at two options for communicating with the network and, of course, discarding the XmlListModel. If the installation didn’t use only QML, of course, you could just open the socket, but I decided to squeeze everything out of javascript.
On the one hand, without this, the application would probably not have become as interesting as it is now. On the other - I had a lot of problems. I’ll tell you about this.
When I decided to use XMLHttpRequest, I immediately realized that you can forget about direct connections between clients, I need a server. To write the server itself, we had to forget about QML / JS, but it’s for the better - the server can hardly be called a “simple” application. I wrote it on okamla, and the only reason for such an absurd act was the fact that it was the Ocsigen server that I trustfully looks into the world with the 80th port. The logic of the server is quite simple and is described by the following facts:
While everyone is watching the codeserver, I will draw your attention to the last point. In order not to have fun on the server with JSON generation, and so as not to drive customers with bad Internet into melancholy, I sent all the data using the protocol that I absolutely idiotic invented. It was my big mistake, try not to make it. The first 16 bytes of the response always contained the session id (regular ascii characters), and then the code for each byte was the corresponding value. That is, in the answer the characters \ 0, \ 1 and the like were regularly found. If I knew javascript better, I would have guessed not to do this, but I have already done stupid things. As it turned out, javascript is not able to work with bytes in strings, only with characters. He digested unprintable characters without hesitation. Zero byte swallowed, not even choking.
I rummaged through the Internet, I found an instruction - forcibly change the response encoding to x-custom-charset - I found that the js engine in Qt does not allow this, and changed the encoding directly on the server. After that, I downloaded the ready-made library BinaryReader.js, which assured me that I was able to read text by bytes and tried to read the result to her as well. Everything was in vain - JS stubbornly gave me 0xff instead of my byte. Fortunately, I found that the indices of all cells allowed for chips are even. I began to divide them in half during transmission, and this allowed me to read the data as it should. As a result, the code acquired another component of 170 lines, which interacted with the server and allowed you to enjoy a full-fledged network game, and I approached the last problem - about it immediately after the advertisement.
At that moment, finally, a thought that had long overwhelmed me, finally took shape. “Dude,” she said, “how could we compile the code?” The question was relevant - I ran all the code for the test through qmlviewer from qtcreator, but of course, this is not an option. I wanted to make the application accessible to more mundane people - who do not have qmlviewer. And then I got a blow from the sub. It turned out that despite the presence of fully QML projects in qtcreator, it is impossible to compile QML. No way. It is necessary to write a viewer in C ++, and from it load the main QML file. This, generally speaking, is a big trouble, in my opinion. I remember once, when the flash was still far from all the machines, the developers at Macromedia did a very tricky and interesting thing: in Flash Player with an open swf file, you could click a button and “compile” a self-sufficient exe file, which was already launched on any machine. Developers from Nokia would not hurt to borrow this wonderful idea, adjusted for different architectures and platforms.
Well, I thought, in the end the whole code works without pluses, so be it, let there be a player on the pluses. I created another project in qtcreator - this time from the category “C ++ and QML”, and began to watch how it compiles using an automatically generated example. According to the results of the inspection, an interesting fact was discovered. In the example project, the QML files were added to the / qml / tzaar / directory . And in main.cpp there was a line:
But in the .pro file there were interesting lines:
They meant neither more nor less, but the fact that the contents of / qml / tzaar / are copied to / qml / when installing the program. Catch it? The example code was valid exactly until the moment I wanted to install it somewhere, after installation the player would no longer find the files. Moreover, editing the .pro file did not help - any attempt to put the same values in source and target led to the fact that qmake went crazy and said that I was trying to copy the file to myself. This alignment obviously did not suit me. I tried to put all the resources in the QRC - if they are in one file, they simply will not be able to get lost. It turned out that here’s the catch: the qmlapplicationviewer class automatically applied to the new project spoiled all qrc: / links. I already found myself mentally ready to fix this error, and all resources, including the html-page with a description of the game, moved to qrc. Everything worked, but now every qtcreator start tells me that my qmlapplicationviewer is different from it,
Corrections for the standard qmlapplicationviewer, so that it becomes correct, can be taken here , but for now I will remind you of a few simple rules for those who want to follow my example:
Following this simple technique, you, of course, lose the notorious "flexibility" and the ability to replace QML without recompilation - but does the game need it? But you will never suffer from the fact that in different distributions and systems the shared data lies at different addresses. Of the alternative solutions, you can: rewrite the entire assembly system to CMake or climb into the guts of the .pri file from QmlApplicationViewer, which is even worse. For some reason, it seems to me that generating a resource file and fixing several paths is a much simpler solution.
Interfaces in QML are designed really very quickly and easily, even by interface haters like me.
In QML, you can write a game or application that works with an online service. If you carefully go around the hidden rake, then the simplicity and convenience will pleasantly surprise you.
With a bit of desire, you can make one nice binary with all the included resources out of all the code, which you can distribute.
Working with the network is still lame. It seems to me that developers of games for mobile devices (especially in the light of NFC PR and the like) would be happy if they had the opportunity to establish a normal connection between devices without getting out to the C ++ level
The intrinsic example in qtcreator is broken. This is a terrible minus. When the attached WorldWorld IDE works incorrectly, it casts a terrible shadow over the entire library. In this case, it is a run over to the developers of QtCreator, and not the technology itself. However, keep in mind. Perhaps, in versions later than mine 2.2.1, this problem was fixed.
Judging by the sawed-off synchronous mode in XMLHttpRequest and the crooked work with byte reading, I got the feeling that the JS engine in Qt is damaged in some places. Be careful.
Those who want to play this wonderful game can read the rules (perhaps their Russian version from the Game Engineer will be easier to understand), compile the game with qmake && make, and play - just remember that you need a partner who will also connect to the server.
Users of 32-bit Ubuntu 11.10 (maybe I don’t promise other systems) can download the archive: sorokdva.net/tzaar/tzaar.tar.gz and start the already assembled binary. To work, you need the libqtwebkit-qmlwebkitplugin package.
And if someone suddenly thought that I was heavily scolding QML, then I will remind you of two points. First: in QML I wrote a game in my free time for a total of a maximum of 40 hours (actually less). Second: on traditional Qt and with its work with graphics, I could not write a game . And this should already speak for itself. But I’m not really a neosilator.
Friday, as you know, is the end of the work week, the best time for fun and games. And the best games are those that are exciting, in which you need to think a little, and which you can play with friends. I decided to write one of these games.
This post is not another translation or a free presentation of a variety of QML Howto and Quick Start. Rather, it is a description of the pitfalls that you may encounter when writing a real application.
When Qt Quick / QML was just announced, Nokia sounded the words that “in the future, not only the user interface will be written in Qt Quick, but all the logic of simple applications will be written in javascript, programmers will not need to write a single line of code on the pros”. The statement was even more provocative than my headline, and immediately interested me: I decided to try to write a simple game without a single line of code on the pluses.
To stir up interest, I add that:
- I usually write the code on the pros
- I know JS poorly enough
- I do not know how and hate to do interfaces
- I once tried to make the same game on honest Qt, but it broke, unable to withstand communication with QGraphicsScene and other interesting classes
- the result of my labors can not only be downloaded, but also played online
- all sources can be downloaded from bazaar or tarball .
We learn about the rest under the cut.
Version one, or the war with drag and drop
As a starting point, the Tsaar game was chosen - a board logic game for two players. I sincerely hope that my insidious implementation of this game will not cause the indignation of the copyright holders, but nevertheless I recommend those interested to buy it - in live form it is much more interesting than in my performance.
And we are back to development. The first thing I saw for ten minutes was the playing field (in the figure above): in all decent games it is, as you know, rectangular. After ten minutes, I said: “Aha!”, I added nodes to the picture so that a rectangular grid formed, and then I wrote a function to check if the node actually exists.
The second problem was the pictures. As I said, I hate making interfaces, and as I draw, it's better not to think at all. Generating a picture in runtime seemed to me even more stupid. I sighed, opened inkscape, and tried to accurately draw the playing field with straight lines. On the third line, I gave up, opened the resulting .svg file in vima and, through simple calculations and manipulations with macros, straightened the existing lines and finished the missing ones. In a similar fashion, six chip images were created. That is why they are so crooked and unlike the original.
I paint my existential throwings in such detail and bring in so little code to add right now: these are the biggest problems that I encountered in the process. It really is. Position the field on the form, generate and arrange chips, make homemade buttons and display status - no problem! Animate moving chips - 2 lines! Allow and prohibit the player to move various chips depending on the phase of the move - all this is done in QML so elementary that I can only offer to read the official manual with examples. By the end of the work, the js-file with all the basic logic, including empty lines and comments, occupied as many as 173 lines or 6 functions.
Oh no, perhaps I remembered one moment that amazed me and forced me to write a crutch. This moment is called:drag'n'drop. Yes, that sounds strange, but a drag and drop in a slightly less than fully graphical toolkit is done badly. It’s possible to “drag”, not an element, but MouseArea, which lies on it. The only thing we can do is to determine which buttons you can use and what restrictions we have on coordinates. It is impossible, as when working with the system, to handle the event "they threw something at me, what is it", you cannot allow an element to throw only at certain objects. You can only handle pressed and released events. Spin further as you want. And in the examples, if my memory serves me, such stupid things are generally dealt with only with all sorts of Grids and Lists, no arbitrary positioned elements for you. Apparently because of this, by the way, it is impossible to tell the element “I didn’t like the way you were thrown, return to the place”. I'm telling you,
Therefore, I had to do the following. The property of the element, obviously, is not its coordinates on the screen - x and y - but the position on the board. Coordinates are calculated based on the position. With the pressed and released events, we remember the initial position and calculate which new one we tried to throw into the element. After that, we call the function responsible for moving the element. If the function tells us that moving is not possible, we need to do with the element what? Right, return to the starting position. Watch your hands carefully:
if (
(oX == nX && oY == nY) // Move to itself
|| board[nI] == null // or move to empty position
|| !canMove(oX, oY, nX, nY) // No way from the old position to the new
) {
board[oI].posX = -1;
board[oI].posY = -1;
board[oI].posX = oX;
board[oI].posY = oY;
return false;
}
See this assignment minus one? Yeah. The fact is that in QML, if you assign a property the value that already lies in it (oX and oY), the engine considers that the property has not changed, and does not recalculate everything that is associated with it, in our case it’s absolute coordinates on the screen. We have to assign some obviously different value, and only then the original one.
The drag-n-drop implementation itself looks like this :
MouseArea {
...
acceptedButtons: Qt.LeftButton
// представляете, тащим мы MouseArea, а перетаскивается — фишка
drag.target: tzaarItem
// ограничиваем перемещение только игровым полем
drag.minimumX: (tzaarItem.parent.width - tzaarItem.parent.paintedWidth)/2
drag.maximumX: (tzaarItem.parent.width + tzaarItem.parent.paintedWidth)/2
drag.minimumY: (tzaarItem.parent.height - tzaarItem.parent.paintedHeight)/2
drag.maximumY: (tzaarItem.parent.height + tzaarItem.parent.paintedHeight)/2
drag.axis: Drag.XandYAxis
// при начале драга мы фишку "поднимаем", чтобы её не перекрыло никакое поле
onPressed: { tzaarItem.z = 10; }
onReleased: {
// при дропе опускаем на место и определяем из её новых координат положение на доске
tzaarItem.z = 1;
var oldPosX = tzaarItem.posX;
var oldPosY = tzaarItem.posY;
var posX = tzaarItem.posXofX(tzaarItem.x);
var posY = tzaarItem.posYofY(tzaarItem.y);
tzaarItem.parent.movePiece(oldPosX, oldPosY, posX, posY);
}
}
And this is where all the problems really ended. One could play the game, win for white, win for black. But - on one computer. It was boring, I longed for games on the network with friends. And here comes the second iteration of development, which is much more interesting.
Version two, or the network strikes back.
Where at this moment I began to think about the third problem, but so far it has not yet reached it, we will talk about the network.
As I discovered, QML has very poor and sad network support. We have Ajax requests from the javascript engine (moreover, only asynchronous), and we have XmlListModel in our examples, which is actively ragged. I would not want to believe that all QML was created solely for easy parsing of RSS feeds.
Anyway, looking ahead, I’ll say that the most obvious illustration of the poverty of working with a network in QML is the following line:
Component.onDestruction: disconnect(); // yes, it will never work, but I have to try
In short, when closing the game, I would like to send a message to the server that I disconnected and the session can be killed. The whole problem is that when a signal arrives, I create an asynchronous ajax request, “send” it, and then ... and then our event loop successfully continues to work and normally terminates the program - because the cause of the signal was a pressed cross in the corner window. Voila! The request will never actually reach the server. Never. But I try, I believe in the best.
In the meantime, this is all in the future, now I'm still looking at two options for communicating with the network and, of course, discarding the XmlListModel. If the installation didn’t use only QML, of course, you could just open the socket, but I decided to squeeze everything out of javascript.
On the one hand, without this, the application would probably not have become as interesting as it is now. On the other - I had a lot of problems. I’ll tell you about this.
When I decided to use XMLHttpRequest, I immediately realized that you can forget about direct connections between clients, I need a server. To write the server itself, we had to forget about QML / JS, but it’s for the better - the server can hardly be called a “simple” application. I wrote it on okamla, and the only reason for such an absurd act was the fact that it was the Ocsigen server that I trustfully looks into the world with the 80th port. The logic of the server is quite simple and is described by the following facts:
- the server accepts requests from anonymous users, generates session id for them and either selects a partner from the waiting ones (ideally there can be no more than one), or queues the partner's expectations
- when the game starts, it is the server that now generates the initial arrangement of chips and sends it to both players
- the player sends a message about his move of the chip, and it is sent to his partner
- once per second each player requests information about fresh actions from the server
- if no requests were received from the player within 3 seconds, he is considered missing and the game breaks off
- any possible error (incorrect data, missing opponent, etc.) is sent to the player an error, after which the client disconnects and displays the ugly message "Some error occured"
- The client sends all the data in the GET fields (query string), and the answer comes in binary form
While everyone is watching the codeserver, I will draw your attention to the last point. In order not to have fun on the server with JSON generation, and so as not to drive customers with bad Internet into melancholy, I sent all the data using the protocol that I absolutely idiotic invented. It was my big mistake, try not to make it. The first 16 bytes of the response always contained the session id (regular ascii characters), and then the code for each byte was the corresponding value. That is, in the answer the characters \ 0, \ 1 and the like were regularly found. If I knew javascript better, I would have guessed not to do this, but I have already done stupid things. As it turned out, javascript is not able to work with bytes in strings, only with characters. He digested unprintable characters without hesitation. Zero byte swallowed, not even choking.
I rummaged through the Internet, I found an instruction - forcibly change the response encoding to x-custom-charset - I found that the js engine in Qt does not allow this, and changed the encoding directly on the server. After that, I downloaded the ready-made library BinaryReader.js, which assured me that I was able to read text by bytes and tried to read the result to her as well. Everything was in vain - JS stubbornly gave me 0xff instead of my byte. Fortunately, I found that the indices of all cells allowed for chips are even. I began to divide them in half during transmission, and this allowed me to read the data as it should. As a result, the code acquired another component of 170 lines, which interacted with the server and allowed you to enjoy a full-fledged network game, and I approached the last problem - about it immediately after the advertisement.
Version three, or where to shove?
At that moment, finally, a thought that had long overwhelmed me, finally took shape. “Dude,” she said, “how could we compile the code?” The question was relevant - I ran all the code for the test through qmlviewer from qtcreator, but of course, this is not an option. I wanted to make the application accessible to more mundane people - who do not have qmlviewer. And then I got a blow from the sub. It turned out that despite the presence of fully QML projects in qtcreator, it is impossible to compile QML. No way. It is necessary to write a viewer in C ++, and from it load the main QML file. This, generally speaking, is a big trouble, in my opinion. I remember once, when the flash was still far from all the machines, the developers at Macromedia did a very tricky and interesting thing: in Flash Player with an open swf file, you could click a button and “compile” a self-sufficient exe file, which was already launched on any machine. Developers from Nokia would not hurt to borrow this wonderful idea, adjusted for different architectures and platforms.
Well, I thought, in the end the whole code works without pluses, so be it, let there be a player on the pluses. I created another project in qtcreator - this time from the category “C ++ and QML”, and began to watch how it compiles using an automatically generated example. According to the results of the inspection, an interesting fact was discovered. In the example project, the QML files were added to the / qml / tzaar / directory . And in main.cpp there was a line:
viewer.setMainQmlFile(QLatin1String("qml/tzaar/main.qml"));
But in the .pro file there were interesting lines:
# Add more folders to ship with the application, here
folder_01.source = qml/tzaar
folder_01.target = qml
DEPLOYMENTFOLDERS = folder_01
They meant neither more nor less, but the fact that the contents of / qml / tzaar / are copied to / qml / when installing the program. Catch it? The example code was valid exactly until the moment I wanted to install it somewhere, after installation the player would no longer find the files. Moreover, editing the .pro file did not help - any attempt to put the same values in source and target led to the fact that qmake went crazy and said that I was trying to copy the file to myself. This alignment obviously did not suit me. I tried to put all the resources in the QRC - if they are in one file, they simply will not be able to get lost. It turned out that here’s the catch: the qmlapplicationviewer class automatically applied to the new project spoiled all qrc: / links. I already found myself mentally ready to fix this error, and all resources, including the html-page with a description of the game, moved to qrc. Everything worked, but now every qtcreator start tells me that my qmlapplicationviewer is different from it,
Corrections for the standard qmlapplicationviewer, so that it becomes correct, can be taken here , but for now I will remind you of a few simple rules for those who want to follow my example:
- if the file was at qml / Tzaar.qml relative to the root of the project, then in qrc it should be searched as qrc: /qml/Tzaar.qml
- if the QML files were all nearby, then they can be loaded directly along the relative path, for example: “Piece.qml” - that is, you won’t have to make any changes
- if we from QML want to load a file that was in another directory (say, field.js, which we had in the root of the project, and not in the qml folder), then we just need to write "/field.js" - that is, add a slash at the beginning of the address, make the path absolute
Following this simple technique, you, of course, lose the notorious "flexibility" and the ability to replace QML without recompilation - but does the game need it? But you will never suffer from the fact that in different distributions and systems the shared data lies at different addresses. Of the alternative solutions, you can: rewrite the entire assembly system to CMake or climb into the guts of the .pri file from QmlApplicationViewer, which is even worse. For some reason, it seems to me that generating a resource file and fixing several paths is a much simpler solution.
Total
Interfaces in QML are designed really very quickly and easily, even by interface haters like me.
In QML, you can write a game or application that works with an online service. If you carefully go around the hidden rake, then the simplicity and convenience will pleasantly surprise you.
With a bit of desire, you can make one nice binary with all the included resources out of all the code, which you can distribute.
Working with the network is still lame. It seems to me that developers of games for mobile devices (especially in the light of NFC PR and the like) would be happy if they had the opportunity to establish a normal connection between devices without getting out to the C ++ level
The intrinsic example in qtcreator is broken. This is a terrible minus. When the attached WorldWorld IDE works incorrectly, it casts a terrible shadow over the entire library. In this case, it is a run over to the developers of QtCreator, and not the technology itself. However, keep in mind. Perhaps, in versions later than mine 2.2.1, this problem was fixed.
Judging by the sawed-off synchronous mode in XMLHttpRequest and the crooked work with byte reading, I got the feeling that the JS engine in Qt is damaged in some places. Be careful.
Those who want to play this wonderful game can read the rules (perhaps their Russian version from the Game Engineer will be easier to understand), compile the game with qmake && make, and play - just remember that you need a partner who will also connect to the server.
Users of 32-bit Ubuntu 11.10 (maybe I don’t promise other systems) can download the archive: sorokdva.net/tzaar/tzaar.tar.gz and start the already assembled binary. To work, you need the libqtwebkit-qmlwebkitplugin package.
And if someone suddenly thought that I was heavily scolding QML, then I will remind you of two points. First: in QML I wrote a game in my free time for a total of a maximum of 40 hours (actually less). Second: on traditional Qt and with its work with graphics, I could not write a game . And this should already speak for itself. But I’m not really a neosilator.