
В этой статье я хотел бы поделиться своим опытом разработки одного виджета (элемента графического интерфейса), попутно осветив некоторые технологии и техники 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
