
Люблю в свободное время что-нибудь прототипировать. Это позволяет поизучать что-то новое. Данный прототип является клиентом для ресурса http://www.nonograms.ru/, разработчиком которого является Чугунный К.А/ KyberPrizrak /. Весь код доступен на GiHub. На стороне C++ работа с HTML, модель галереи. На стороне QtQuick визуализация.
В этот раз решил поковырять:
- Q_GADGET и его использование в Qml;
- есть ли жизнь без Qt WebKit;
поковырять Qt Labs Controls.
Что сделано:
- галерея кроссвордов;
- разгадывание кроссворда.
Под катом будет рассмотрено:
- скриншоты;
- как получить HTML без Qt WebKit;
- как сделать кроссворд без Canvas.


Обходимся без Qt WebKit
Сайт отдает кроссворд в виде матрицы:
var d=[[571,955,325,492], [6,53,49,55], [47,18,55,65], ...]]
Дальше JS скрипы создают html код кроссворда. Модуль WebKit был помечен как deprecated. В замен него предлагается использовать модуль Web Engine основанный на проекте Chrome.
Тут сразу ждет небольшое разочарование. Web Engine не имеет API для работы с DOM на странице. Для разбора HTML кода пришлось воспользоваться сторонними средствами(Парсим HTML на C++ и Gumbo).
А вот загрузить страницу, отрендерить и получить нужный HTML мы можем.
QString getHtml(const QUrl& url) { QWebEnginePage page; QEventLoop loop; QObject::connect(&page, &QWebEnginePage::loadFinished, &loop, &QEventLoop::quit); page.load(url); loop.exec(); QTimer::singleShot(1000, &loop, &QEventLoop::quit); QString html; page.toHtml([&html, &loop](const QString& data){ html = data; loop.quit(); }); loop.exec(); return html; }
QTimer::singleShot здесь используется для ожидания когда страница достроится. Метод toHtml асинхронный и принимает в качестве входного параметра функцию обратного вызова, для получения результата.
Построение кроссворда

Кроссворд решил представить как множество столбцов и строчек. Наверху красным обведено 10 столбцов, каждый размера 3. Слева обведены 10 строк, каждая размером 3. Далее код будет оперировать этими величинами.
Кроссворд можно сделать несколькими способами:
- рисовать на C++;
- рисовать на JS и Canvas;
- построить из базовых элементов(Item, Rectangle, MouseArea и т.д.)
Я выбрал последний вариант.
import QtQuick 2.5 import Qt.labs.controls 1.0 Item { clip:true property int margin: 20 property int fontSize: 12 property int ceilSize: 20; property int incCeilSize: ceilSize + 1 property color borderColor: "#424242" property int rows: 0; property int rowSize: 0; property int column: 0; property int columnSize: 0; implicitHeight : crossGrid.height+margin*2 implicitWidth : crossGrid.width+margin*2 function loadFromNonogramsOrg(url) { console.log("Load:"+url); crossword.formNanogramsOrg(url); } function showOnlyNaturalNumber(val) { return val > 0 ? val: " "; } function drawCrossword(){ var csize = crossword.size; if(csize.column() === 0 || csize.rows() === 0){ return; } console.log(csize.column() + "x" + csize.rows()); hRepeater.model = 0; rRepeater.model = 0; rowSize = crossword.rowSize(); columnSize = crossword.columnSize(); rows = csize.rows(); column = csize.column(); hRepeater.model = crossword.columnSize()*csize.column(); rRepeater.model = crossword.rowSize()*csize.rows(); bgImg.visible = true; } Image{ id: bgImg asynchronous: true visible: false height: parent.height width: parent.width source:"qrc:/wall-paper.jpg" } Grid { id: crossGrid anchors.centerIn: parent columns: 2 spacing: 2 rowSpacing: 0 columnSpacing: 0 Rectangle{ id:topLeftItm width: rowSize * ceilSize height:columnSize * ceilSize border.width: 1 border.color: borderColor color: "transparent" } Grid { id: cGrid rows: columnSize columns: column Repeater { id: hRepeater model: 0 Item { width: ceilSize; height: ceilSize property int rw : Math.floor(index/column) property int cn : Math.floor(index%column) property int prw: rw+1 property int pcm: cn+1 Rectangle{ height: (prw % 5 == 0) || (prw == columnSize) ? ceilSize : incCeilSize width: (pcm % 5 == 0) ? ceilSize : incCeilSize color: "transparent" border.width: 1 border.color: borderColor Text { anchors.centerIn: parent text:showOnlyNaturalNumber( crossword.columnValue(cn,rw)); font{ family: mandarinFont.name pixelSize: fontSize } } } } } } Grid { id: rGrid rows: rows columns: rowSize Repeater { id: rRepeater model: 0 Item { width: ceilSize; height: ceilSize property int rw : Math.floor(index/rowSize) property int cn : Math.floor(index%rowSize) property int prw: rw+1 property int pcn: cn+1 Rectangle{ height: prw % 5 == 0 ? ceilSize : incCeilSize width: (pcn % 5 == 0) || (pcn == rowSize) ? ceilSize : incCeilSize color: "transparent" border.width: 1 border.color: borderColor Text { anchors.centerIn: parent text:showOnlyNaturalNumber( crossword.rowValue(rw,cn)); font{ family: mandarinFont.name pixelSize: fontSize } } } } } } Rectangle{ id: playingField width: column * ceilSize height:rows * ceilSize border.width: 1 border.color: borderColor color: "transparent" Grid{ rows: rows columns:column Repeater { id: bRepeater model: rows * column Item { id: ceilItm width: ceilSize; height: ceilSize property int rw : Math.floor(index/column) property int cn : Math.floor(index%column) state: "default" Rectangle{ id: itmRec height: (rw+1) % 5 == 0 ? ceilSize : incCeilSize width: (cn+1) % 5 == 0 ? ceilSize : incCeilSize color: "transparent" border.width: 1 border.color: borderColor } Text{ id: itmTxt visible:false height: parent.height width: parent.width font.pixelSize: ceilSize horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text:"+" rotation:45 } MouseArea { anchors.fill: parent onClicked: { if(parent.state == "default"){ parent.state = "SHADED"; }else if(parent.state == "SHADED"){ parent.state = "CLEAR"; }else{ parent.state = "default"; } } } states: [ State{ name:"SHADED" PropertyChanges { target: itmRec; color: "black"; } PropertyChanges { target: itmTxt; visible: false; } }, State{ name:"CLEAR" PropertyChanges { target: itmRec; color: "transparent"; } PropertyChanges { target: itmTxt; visible: true; } } ] } } } } } Text{ visible: bgImg.visible anchors{ right: parent.right rightMargin: 10 bottom: parent.bottom } text:qsTr("Source: ")+"www.nonograms.ru" font{ family: hanZiFont.name pixelSize: 12 } } Connections { target: crossword onLoaded: { drawCrossword(); } } }
Основа представлена Item, размер которого вычисляется из размера crossGrid и размера отступа(margin)
Item { clip:true implicitHeight : crossGrid.height+margin*2 implicitWidth : crossGrid.width+margin*2 /* ... */ Image{ id: bgImg asynchronous: true visible: false height: parent.height width: parent.width source:"qrc:/wall-paper.jpg" } Grid { id: crossGrid anchors.centerIn: parent columns: 2 spacing: 2 /* ... */ } }
Элемент crossGrid

Grid { id: crossGrid anchors.centerIn: parent columns: 2 spacing: 2 rowSpacing: 16 columnSpacing: 16 Rectangle{ id:topLeftItm color: "transparent" border.width: 1 border.color: borderColor /* ... */ } Grid { id: cGrid /* ... */ } Grid { id: rGrid /* ... */ } Rectangle{ id: playingField /* ... */ } }
topLeftItm прямоугольник заполняющий пространство. cGrid и rGrid описывают сетку с числами. playingField поле для решения кроссворда.
Построение сетки
Если написать так:
Grid { id: cGrid rows: columnSize columns: column Repeater { id: hRepeater /* ... */ Item { width: ceilSize; height: ceilSize Rectangle{ height: ceilSize width: ceilSize color: "transparent" border.width: 1 border.color: borderColor Text { anchors.centerIn: parent text: index font{ family: mandarinFont.name pixelSize: fontSize } } } } } }
то получим удвоение линии

Что бы убрать удвоение линии используем трюк с размерами Item и Rectangle. Размер Item фиксирован, для того что бы в повторителе(Repeater) все элементы располагались ровно. Rectangle шире и выше на единицу, в зависимости от необходимости двойной линии.
Repeater { id: hRepeater model: 0 Item { width: ceilSize; height: ceilSize property int rw : Math.floor(index/column) property int cn : Math.floor(index%column) property int prw: rw+1 property int pcm: cn+1 Rectangle{ height: (prw % 5 == 0) || (prw == columnSize) ? ceilSize : incCeilSize width: (pcm % 5 == 0) ? ceilSize : incCeilSize color: "transparent" border.width: 1 border.color: borderColor Text { anchors.centerIn: parent text:showOnlyNaturalNumber( crossword.columnValue(cn,rw)); font{ family: mandarinFont.name pixelSize: fontSize } } } } }
Тут на основе индекса вычисляется строка(rw) и колонка(cn), увеличиваются на единицу, берется остаток от деления на 5. Т.е. через каждые 5 клеток ширина или высота Rectangle и Item совпадают, что дает удвоение линии.
Поле кроссворда
От поля нам нужна сетка и обработка щелчка мыши. Введем состояние ячейки сетки:
- неактивная(default);
- закрашенная(SHADED);
- помеченная пустой(CLEAR).
Начинать будем c неактивного состояния и менять по клику мыши в следующей последовательности

Код рисования ячейки:
Item { id: ceilItm width: ceilSize; height: ceilSize property int rw : Math.floor(index/column) property int cn : Math.floor(index%column) state: "default" Rectangle{ id: itmRec height: (rw+1) % 5 == 0 ? ceilSize : incCeilSize width: (cn+1) % 5 == 0 ? ceilSize : incCeilSize color: "transparent" border.width: 1 border.color: borderColor } Text{ id: itmTxt visible:false height: parent.height width: parent.width font.pixelSize: ceilSize horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text:"+" rotation:45 } MouseArea { anchors.fill: parent onClicked: { if(parent.state == "default"){ parent.state = "SHADED"; }else if(parent.state == "SHADED"){ parent.state = "CLEAR"; }else{ parent.state = "default"; } } } states: [ State{ name:"SHADED" PropertyChanges { target: itmRec; color: "black"; } PropertyChanges { target: itmTxt; visible: false; } }, State{ name:"CLEAR" PropertyChanges { target: itmRec; color: "transparent"; } PropertyChanges { target: itmTxt; visible: true; } } ] }
itmTxt элемент добавляющий крестик на ячейку, отображая её как помеченную пустой. Тут вовсю используется возможность описывать различные состояния через states.
MouseArea осуществляет переход. То из-за чего все затевалось. Никаких расчетов(преобразования координаты мыши в ячейку сетки), никаких ручных перерисовок.