Японские кроссворды на QtQuick
Люблю в свободное время что-нибудь прототипировать. Это позволяет поизучать что-то новое. Данный прототип является клиентом для ресурса http://www.nonograms.ru/, разработчиком которого является Чугунный К.А/ KyberPrizrak /. Весь код доступен на GiHub. На стороне C++ работа с HTML, модель галереи. На стороне QtQuick визуализация.
В этот раз решил поковырять:
Под катом будет рассмотрено:
- скриншоты;
- как получить 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 осуществляет переход. То из-за чего все затевалось. Никаких расчетов (преобразования координаты мыши в ячейку сетки), никаких ручных перерисовок.