Qt виджет для добавления и удаления строк в таблицe QTableView

в 8:18, , рубрики: c++, GUI, mvc, opensource, qt, Qt Software, Программирование, метки: , , ,

Qt виджет для добавления и удаления строк в таблицe QTableView
В этой статье я хотел бы поделиться своим опытом разработки одного виджета (элемента графического интерфейса), попутно осветив некоторые технологии и техники Qt.
Очень часто бывает необходимо дать пользователю возможность вставки строк и столбцов в таблицу или удаление их из неё. Как правило это реализуется так: надо выделить строку кликнув по хидеру и выбрать пункт в меню: select > menu > insert | delete. Это не совсем очевидно и интуитивно, как и то что строка вставляется перед текущей а не, например, после неё. Поэтому я написал виджет который снимает эту проблему.
Виджет выглядит как четыре кнопки, следующие за курсором по границе таблицы (хаха, это совсем как те пчелки, которые бегали за курсором на сайтах в эпоху вэб 1.0!). Можно было бы перегрузить QTableView, но тогда придется изменять все инстансы; вместо этого я написал отдельный виджет которые представляет из себя как бы панельку которая прикрепляется к уже имеющемуся QTableView.
Проект представляет из себя четыре класса, основные: кнопка InsertRemove::Button, панель InsertRemove::Panel и два вспомогательных для демонстрации возможностей: контейнер для хранения матрицы данных основаный на векторе векторов InsertRemove::Matrix и модель — интерфейс этого контейнера InsertRemove::Model.
Кнопка вычисляет адекватное текущей координате и текущему значению политики (InsertRemove::Policy) положение на виджете, рассчитывая его из ширины столбцов и высоты строк. Одна координата — сумма этих размерностей, другая — ближайшее значение границы или середины строки.

    int coord1;
    int coord2 = 0;

    int sizes[m];
    if (_orientation == Qt::Horizontal)
    {
        for (int i=0;i<m;i++) 
            sizes[i] = table->columnWidth(i);
        for (int i=0;i<n;i++) 
            coord2 += table->rowHeight(i);
    }
    else
    {
        for (int i=0;i<m;i++) 
            sizes[i] = table->rowHeight(i);
        for (int i=0;i<n;i++) 
            coord2 += table->columnWidth(i);
    }

    if (_type == InsertRemove::Insert)
        nearestBorder(_policy,point1+offset1,sizes,m,&_modelIndex,&coord1);
    else // _type == InsertRemove::Remove
        nearestMiddle(_policy,point1+offset1,sizes,m,&_modelIndex,&coord1);

Политики такие: вставка, удаление, вставка только в начало, вставка только в конец, ведь не во всех моделях можно позволять все эти манипуляции.
Для кнопки я применил широко используемый в Qt концепт родителя (parent concept), для объектов это значит что при удаления родителя удаляются все дочерние объекты (memory management), а для виджетов кроме того ещё и то, что дочерние виджеты отображаются в пределах родителя (если не являются диалоговыми окнами) и в его координатной системе.
Для описания стиля нет ничего лучше чем css, а для хранения данных неплохо бы использовать ресурсную систему Qt, которая позволяет эмбедить ресурсы в бинарник и обращаться к ним как к файлам, используя ':' в качестве корневой директории.

    QString plus_css =
            "*         {image: url(':/plus-icon.png'); border: 0;}"
            "*:hover   {image: url(':/plus-icon-hover.png');}"
            "*:pressed {image: url(':/plus-icon-pressed.png');} ";

    QString minus_css =
            "*         {image: url(':/minus-icon.png'); border: 0;}"
            "*:hover   {image: url(':/minus-icon-hover.png');}"
            "*:pressed {image: url(':/minus-icon-pressed.png');} ";

    if (_type == Insert)
        setStyleSheet(plus_css);
    else
        setStyleSheet(minus_css);

При нажатии кнопки вызвается собственно вставка или удаление в модели.

void Button::on_clicked()
{
    QTableView* table = dynamic_cast<QTableView*>(this->parent());
    if (!table)
        return;

    QAbstractItemModel* model = table->model();
    if (!model) 
        return;

    if (_type == InsertRemove::Insert)
    {
        if (_orientation == Qt::Horizontal)
            model->insertColumn(_modelIndex);
        else
            model->insertRow(_modelIndex);
    }
    else // _type == InsertRemove::Remove
    {
        if (_orientation == Qt::Horizontal)
            model->removeColumn(_modelIndex);
        else
            model->removeRow(_modelIndex);
    }
}

Теперь, когда с кнопкой всё ясно, переходим к панели. Она отвечает за создание и хранение этих кнопок и передачу им координат и политик, а так же за удочерение кнопок таблицей.

void Panel::attach(QTableView* table)
{
    for (int i=0;i<4;i++)
        _buttons[i]->setParent(table);
    _table = table;

    table->setMouseTracking(true);
    table->viewport()->installEventFilter(this);
    connect(table->horizontalHeader(),SIGNAL(sectionResized(int,int,int)),this,SLOT(placeButtons()));
    connect(table->verticalHeader(),SIGNAL(sectionResized(int,int,int)),this,SLOT(placeButtons()));
    placeButtons();
}

Мой любимая фича Qt — фильтр событий (QEventFilter), он позволяет, помолясь, вторгнуться во внутреннюю жизнь объекта нарушив тем самым инкапсуляцию. Когда я думаю об этом я мысленно возвращаюсь во времена голых WinApi, криппи макроопределений, структур и ругательств типа lpcwstr, когда десктопным программистом быть было труднее. С помощью такого фильтра панель отслеживает движение мыши по QTableView. Фильтром в моём случае является сама панель. Сначала я хотел сделать фильтр отдельным классом и вынести событие в сигнал, но потом я подумал, что должно быть есть весомая причина почему события это события, а сигналы это сигналы и всё не свалено в одну кучу (как сделано, если мне не изменяет память, в .Net). Во первых события — это внутренняя жизнь виджета, а сигналы — его интерфейс, а во вторых очевидно что события быстрее за счет меньшего числа прослоек (layers of interaction), что может быть критично в нашем случае, когда событие будет вызываться сотни раз в секунду. Хотя я и не уверен на все 100 в данных выкладках. Итак, всё что нам нужно отследить событие движение мыши и передать координаты кнопкам.

bool Panel::eventFilter(QObject* object, QEvent* event)
{
    if (event->type() == QEvent::MouseMove && object == _table->viewport())
    {
        QMouseEvent* mouseEvent = dynamic_cast<QMouseEvent*>(event);
        if (!mouseEvent) 
			return false;
        for (int i=0;i<4;i++)
            _buttons[i]->setPoint(mouseEvent->pos());
    }
    return false;
}

Контейнер и демонстрационная модель кажется самоочевидны, поэтому переходим сразу к применению: создаём модель, создаём таблицу, создаём панель, прикрепляем панель к таблице.

    QTableView view;
    view.setModel(&model);

    Panel panel(EverythingAllowed,EverythingAllowed);
    panel.setPolicy(Qt::Horizontal, (PolicyFlags) RemoveAllowed | AppendAllowed );
    panel.attach(&view);

Одним из недостатков данного виждета является то что он завязан на QTableView (это обусловлено тем, что именно для этого он мне и был нужен), хотя его можно было бы использовать для других типов представлений. Если будет время и желание я решу и этот вопрос.
Возможно, моя работа может пригодиться Вам, её можно взять на гитхабе или с моего сервера. Всё необходимое лежит в папке insertremove в неймспейсе InsertRemove, подключается инклудом в профайле. Для библиотеки маловато и сыровато пока. Fell free to use and contribute.
ссылки:

git clone git://github.com/overloop/insertremovepanel.git
git clone git://mugiseyebrows.ru/insertremovepanel.git

посмотреть: github.com | mugiseyebrows.ru
скачать: github.com | mugiseyebrows.ru

Автор: mugisbrows

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js