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