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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +1
    Для начала сразу скажу, что код не соответствует Qt coding conventions, поправьте.
    Во вторых можно обойтись static_cast'ом вместо dynamic_cast'а. Во всём Qt почти не используют dynamic_cast. С ним есть некоторые сложности при разработки библиотек.
      0
      Спасибо за замечание, убрал rtti.
      0
      Одним из недостатков данного виждета является то что он завязан на QTableView (это обусловлено тем, что именно для этого он мне и был нужен), хотя его можно было бы использовать для других типов представлений. Если будет время и желание я решу и этот вопрос.


      Я думаю можно с минимальным количеством изменений расширить до QAbstractItemView
        –1
        Пост отличный, но в скриншоте все же что-то режет глаз.

          0
          Просто субпиксельное сглаживание шрифтов, включенное у автора, не подходит к вашему монитору.
            +1
            А, я понял, вы о несовпадении линии таблицы.
              +1
              К шрифтам никаких претензий. На линии таблицы посмотрите.
                0
                Простите мне пой праздный интерес, но вы часто так приближаете?
                  +1
                  Никогда так не приближаю специально. Просто листал главную и взгляд сам остановился на скриншоте. Рассмотрев его чуть детальнее сразу понял что в нем не так, без всяких увеличений. Хотя возможно я излишне требователен — к теме поста эта деталь никоим образом отношения не имеет.
            +1
            Костылями даже не просто пахнет. Вот они!
            1. Что за решение — кнопка/(гуи виджет) управляет моделью??? Этим должен заниматься контроллер и только! Мало ли кейсов будет, в которых должны добавляться ячейки!
            2. «Можно было бы перегрузить QTableView, но тогда придется изменять все инстансы»
            С какого перепугу?

            3. А вот почему костыль — вместо перегрузки используется дополнительный виджет, который лезет в ивент фильтр и подменяет часть функционала контроллера.

            нельзя такое в продакшн…
              0
              Не понимаю.
              1) Кнопка и есть контроллер же.
              2) С такого перепуга что вместо view = new QTableView() везде view = new QTableViewWithButtons()
              3) Что значет подменяет
                0
                1.а) А вот вы захотели использовать вашу так красиво нарисованную кнопку в другом месте. Будете делать еще 1 класс, или захотите все же использовать готовое? А если функционал контроля модели вам больше не нужен? Оставите «чтоб было»?
                1.б) А вот вам нужна функция «добавить строку» в обработчике, скажем, генератора таблиц истинности О_О. Будете вызвыать методы ваших гуишных кнопок? Как? Будете создавать виджет и прятать его?

                Дальнейшее использование в иных кейсах невозможно, а это криво.

                2) Слыхали о такой штуке — refactor? В три клика и один копипаст вашь проект уже работает с новым классом.

                3) Кнопка должна только работать с интерфейсами и не должна изменять другие объекты. Смотрите п.1. По поводу ивент фильтра я был не прав. Почему-то подумал, что он в классе кнопки написан.
                  +1
                  1.а) не могу себе представить как ещё можно использовать данную кнопку и в каком месте, её назначение в том чтобы вертеть модель, пылесосить и заваривать кофе она не может и не должна.
                  1.б) если мне нужно будет добавить строку я возьму модель и через неё добавлю строку, при чем здесь гуй вообще?
                  2) objection! Слыхали о такой штуке как ui файлы, создаваемые визуальным редактором, в них придется лазать руками и менять, что кстати приведет к тому что в редакторе они будут смотреться уже не так.
                  3) модель это и есть интерфейс структуры которую она контролирует, если попытаться написать интерфейс к модели весьма вероятно получится точно такая же модель.
                    –2
                    Отстаивать свое решение — хорошо.
                    Однобоко смотреть — плохо.

                    Второго тут излишне.

            Only users with full accounts can post comments. Log in, please.