Череп и кости, КДПВ


Люблю в свободное время что-нибудь прототипировать. Это позволяет поизучать что-то новое. Данный прототип является клиентом для ресурса 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


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 осуществляет переход. То из-за чего все затевалось. Никаких расчетов(преобразования координаты мыши в ячейку сетки), никаких ручных перерисовок.

Поделиться с друзьями
-->

Комментарии (7)


  1. Antervis
    17.05.2016 10:07

    я обычно когда вижу такое в проекте сразу ищу время переписать через модель+делегат.


    1. RPG18
      17.05.2016 11:22

      У меня возникли небольшие трудности с предметной областью, если пользоваться определением из вики

      Изображения зашифрованы числами, расположенными слева от строк, а также сверху над столбцами.

      Но выкрутится в принципе можно.


      1. Antervis
        17.05.2016 11:40

        так ведь ничего же не мешает сделать три модели (для поля, а также левой и верхней шапок), которые будут обращаться к одним и тем же данным. При этом логика моделей шапок будет одинакова с точностью до транспонирования.


        1. RPG18
          17.05.2016 11:43

          Сейчас понимаю, что можно сделать все через одну модель. Если буду делать не прототип, то сделаю через модель.


  1. QtRoS
    17.05.2016 10:18
    +1

    Добрый день, несколько замечаний по статье:
    1. Обычно в статье присутствует вывод, общие впечатления и т.д.
    2. Не очень эффективно делать так:

    height: parent.height
    width: parent.width
    
    В данном случае это байндинги (кстати, оптимизируемые QV4 из-за простоты), anchors быстрее — для повторяющихся элементов и делегатов может быть важно.
    3. Когда модель большая, очень полезно писать не просто имя свойства в модели (роли), а добавлять слово model, например «model.index» вместо «index». Так явно показывается, что данные берутся из модели, а не какой-то одноименной переменной в сущности.
    4. Контрол подобный
    Text {
                            text: qsTr("Author")
                            font.family: hanZiFont.name
                            font.pixelSize: view.labelFontPixelSize
                        }
    
    Используется во многих местах, например 7 раз в Nonogram.qml. Было бы целесообразно выделить его в отдельный компонент, чтобы было легче поддерживать и не нарушать старый добрый DRY.

    В целом получилось неплохо.


    1. RPG18
      17.05.2016 11:40

      Полностью с вами согласен, но пока производительность не беспокоит(если есть аппаратное ускорение OpenGL в драйверах видеокарты), но стал изучать QML Profiler. Это прототип и код еще не раз поменяется. Сейчас ставлю цели:


      • завести на своём планшете с Arndoid;
      • поиграться с версткой под ретиной;
      • поковырять пресловутый material design в связке с Qt Labs Controls.


    1. BlackRaven86
      17.05.2016 19:46
      +1

      2. Не очень эффективно делать так:

      height: parent.height
      width: parent.width

      В данном случае это байндинги (кстати, оптимизируемые QV4 из-за простоты), anchors быстрее — для повторяющихся элементов и делегатов может быть важно.

      Кстати, не всегда. Если размеры должны часто меняться, то да. Например, для фона окна хорошо подходит (если размер окна можно менять, конечно). А вот размер itmTxt зависит от ceilSize и не меняется в процессе работы, так что тут позиционировать про помощи width, height, x и y совершенно нормально. Использование якорей создает дополнительные объекты, поэтому стоит их использовать тогда, когда размер элемента должен часто меняться. Документация.