├── .gitignore ├── imagelistmodel.cpp ├── imagelistmodel.h ├── imagelistview.cpp ├── imagelistview.h ├── imageviewer.ico ├── imageviewer.pro ├── imageviewer.qrc ├── imageviewer.rc ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── three.png └── two.png /.gitignore: -------------------------------------------------------------------------------- 1 | /*.user 2 | -------------------------------------------------------------------------------- /imagelistmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "imagelistmodel.h" 2 | 3 | #include 4 | #include 5 | 6 | ImageListModel::ImageListModel(QObject* parent) 7 | : QAbstractTableModel(parent) 8 | { 9 | imageNameFilter << "*.png" 10 | << "*.jpg" 11 | << "*.gif"; 12 | } 13 | 14 | bool ImageListModel::loadDirectoryImageList(const QString& fullPath) 15 | { 16 | qInfo() << "Loading Image List From " << fullPath << "started"; 17 | QDir directory{ fullPath }; 18 | beginResetModel(); 19 | imageFileInfoList = directory.entryInfoList(imageNameFilter, QDir::Files, QDir::Name); 20 | qInfo() << "Loading Image List From " << fullPath << "finished: " << imageFileInfoList.size() << "images"; 21 | endResetModel(); 22 | return true; 23 | } 24 | 25 | int ImageListModel::rowCount(const QModelIndex& parent) const 26 | { 27 | return parent.isValid() ? 0 : imageFileInfoList.size(); 28 | } 29 | 30 | int ImageListModel::columnCount(const QModelIndex& parent) const 31 | { 32 | return parent.isValid() ? 0 : 1; 33 | } 34 | 35 | QVariant ImageListModel::data(const QModelIndex& index, int role) const 36 | { 37 | if (index.isValid()) { 38 | if (role == Qt::DisplayRole) { 39 | return imageFileInfoList[index.row()].absoluteFilePath(); 40 | } 41 | } 42 | return QVariant(); 43 | } 44 | -------------------------------------------------------------------------------- /imagelistmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGELISTMODEL_H 2 | #define IMAGELISTMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * @brief The ImageListModel class 10 | * ImageListModel - класс модели, содержащей список имен файлов изображений 11 | */ 12 | class ImageListModel : public QAbstractTableModel { 13 | Q_OBJECT 14 | public: 15 | ImageListModel(QObject* parent = Q_NULLPTR); 16 | 17 | // ImageListModel interface 18 | public: 19 | /** 20 | * @brief loadDirectoryImageList 21 | * @param fullPath 22 | */ 23 | bool loadDirectoryImageList(const QString& fullPath); 24 | 25 | // QAbstractItemModel interface 26 | public: 27 | /** 28 | * @brief rowCount возвращает число файлов модели 29 | * @param parent 30 | * @return число файлов модели 31 | */ 32 | virtual int rowCount(const QModelIndex& parent) const override; 33 | /** 34 | * @brief columnCount возвращает число столбцов модели 35 | * @param parent 36 | * @return число столбцов модели 37 | */ 38 | virtual int columnCount(const QModelIndex& parent) const override; 39 | /** 40 | * @brief data возращает данные по индексу модели index и роли role 41 | * @param index 42 | * @param role 43 | * @return данные по индексу модели index и роли role 44 | */ 45 | virtual QVariant data(const QModelIndex& index, int role) const override; 46 | 47 | private: 48 | /** 49 | * @brief imageNameFilter 50 | * Список масок файлов изображений 51 | */ 52 | QStringList imageNameFilter; 53 | /** 54 | * @brief imageFileInfoList 55 | * Список файлов 56 | */ 57 | QFileInfoList imageFileInfoList; 58 | }; 59 | 60 | #endif // IMAGELISTMODEL_H 61 | -------------------------------------------------------------------------------- /imagelistview.cpp: -------------------------------------------------------------------------------- 1 | #include "imagelistview.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | ImageListView::ImageListView(QWidget* parent) 13 | : QAbstractItemView(parent) 14 | , m_columnCount{ 5 } 15 | , m_loadingDelayTimer{ new QTimer{ this } } 16 | , m_updatingDelayTimer{ new QTimer{ this } } 17 | , m_imageCache(1) 18 | { 19 | horizontalScrollBar()->setRange(0, 0); 20 | verticalScrollBar()->setRange(0, 0); 21 | setSelectionMode(ExtendedSelection); 22 | setSelectionBehavior(SelectItems); 23 | 24 | // подписываемся на таймер отложенной загрузки 25 | m_loadingDelayTimer->setSingleShot(true); 26 | connect(m_loadingDelayTimer, &QTimer::timeout, [this] { 27 | qDebug() << "Scroll Delay Timer Fired"; 28 | startAsyncImageLoading(); 29 | }); 30 | connect(&m_imageLoadingFutureWatcher, 31 | &QFutureWatcherBase::progressRangeChanged, [](int min, int max) { 32 | qDebug() << "progressRangeChanged(" << min << ", " << max << ")"; 33 | }); 34 | connect(&m_imageLoadingFutureWatcher, 35 | &QFutureWatcherBase::progressValueChanged, [](int val) { 36 | qDebug() << "progressValueChanged(" << val << ")"; 37 | }); 38 | connect(&m_imageLoadingFutureWatcher, 39 | &QFutureWatcherBase::started, []() { 40 | qDebug() << "started()"; 41 | }); 42 | connect(&m_imageLoadingFutureWatcher, 43 | &QFutureWatcherBase::finished, []() { 44 | qDebug() << "finished()"; 45 | }); 46 | connect(&m_imageLoadingFutureWatcher, 47 | &QFutureWatcherBase::resultReadyAt, 48 | [](int index) { 49 | qDebug() << "resultReadyAt(" << index << ")"; 50 | }); 51 | // подписываемся на результат загрузки 52 | 53 | connect(&m_imageLoadingFutureWatcher, 54 | &QFutureWatcherBase::resultsReadyAt, 55 | [this](int begin, int end) { 56 | qDebug() << "resultsReadyAt: Background Loading for images [" << begin << ":" << end << ") finished"; 57 | for (int index = begin; index < end; ++index) { 58 | auto item = m_imageLoadingFutureWatcher.resultAt(index); 59 | m_invalidatingModelRows.append(item->row); 60 | m_imageCache.insert(item->imageFileName, item->image.release()); 61 | qDebug() << "Loading" << item->imageFileName << "finished"; 62 | } 63 | if (!m_updatingDelayTimer->isActive()) 64 | m_updatingDelayTimer->start(250); 65 | }); 66 | 67 | // 68 | m_updatingDelayTimer->setSingleShot(true); 69 | connect(m_updatingDelayTimer, &QTimer::timeout, [this] { 70 | qDebug() << "Update Delay Timer Fired"; 71 | QRect invalidatingRect; 72 | for (auto&& row : m_invalidatingModelRows) { 73 | auto rect = visualRect(model()->index(row, 0, rootIndex())); 74 | invalidatingRect = invalidatingRect.united(rect); 75 | } 76 | if (viewport()->rect().intersects(invalidatingRect)) { 77 | qDebug() << "Update the " << invalidatingRect << "region starting.."; 78 | viewport()->update(invalidatingRect); 79 | } 80 | }); 81 | } 82 | 83 | void ImageListView::startScrollDelayTimer() 84 | { 85 | qDebug() << "Scroll Delay Timer Restarted"; 86 | stopScrollDelayTimer(); 87 | m_loadingDelayTimer->start(250); 88 | } 89 | 90 | void ImageListView::stopScrollDelayTimer() 91 | { 92 | m_updatingDelayTimer->stop(); 93 | stopAsyncImageLoading(); 94 | m_loadingDelayTimer->stop(); 95 | } 96 | 97 | int ImageListView::columnCount() const 98 | { 99 | return m_columnCount; 100 | } 101 | 102 | void ImageListView::setColumnCount(int columnCount) 103 | { 104 | qDebug() << "Image List View setColumnCount" << columnCount << "called"; 105 | m_columnCount = columnCount; 106 | reset(); 107 | } 108 | 109 | QPair ImageListView::modelRowRangeForViewportRect(const QRect& rect) 110 | { 111 | QRect r = rect.normalized(); 112 | int rowCount = model()->rowCount(rootIndex()); 113 | int begin = 0; 114 | { 115 | QModelIndex startIndex = indexAt(r.topLeft()); 116 | if (startIndex.isValid()) { 117 | begin = startIndex.row(); 118 | } 119 | } 120 | int end = begin; 121 | { 122 | QModelIndex finishIndex = indexAt(r.bottomRight()); 123 | if (finishIndex.isValid()) { 124 | end = finishIndex.row() + 1; 125 | } else { 126 | end = rowCount; 127 | } 128 | } 129 | return QPair(begin, end); 130 | } 131 | 132 | void ImageListView::startAsyncImageLoading() 133 | { 134 | class ImageLoader { 135 | public: 136 | typedef ImageLoadingTaskSharedPtr result_type; 137 | 138 | public: 139 | ImageLoadingTaskSharedPtr operator()(ImageLoadingTaskSharedPtr task) 140 | { 141 | if (!task->image) { 142 | task->image = std::make_unique(); 143 | } 144 | if (task->image->isNull()) { 145 | qDebug() << "ThreadId:" << QThread::currentThreadId() << "Loading" << task->imageFileName << ".."; 146 | if (!task->image->load(task->imageFileName)) { 147 | qWarning() << "Loading" << task->imageFileName << "failed"; 148 | } 149 | } 150 | return task; 151 | } 152 | }; 153 | stopAsyncImageLoading(); 154 | QPair modelRowRange = modelRowRangeForViewportRect(viewport()->rect()); 155 | QList viewportItems; 156 | viewportItems.reserve(modelRowRange.second - modelRowRange.first); 157 | for (int row = modelRowRange.first; row < modelRowRange.second; ++row) { 158 | QModelIndex index = model()->index(row, 0, rootIndex()); 159 | QVariant imageFileNameVariant = model()->data(index); 160 | QString imageFileName = imageFileNameVariant.toString(); 161 | ImageLoadingTask item{ row, imageFileName }; 162 | QImage* ptr = m_imageCache.take(imageFileName); 163 | if (ptr) { 164 | item.image.reset(ptr); 165 | } 166 | viewportItems << std::make_shared(std::move(item)); 167 | } 168 | QFuture future = QtConcurrent::mapped(viewportItems, ImageLoader{}); 169 | m_imageLoadingFutureWatcher.setFuture(future); 170 | } 171 | 172 | void ImageListView::stopAsyncImageLoading() 173 | { 174 | qDebug() << "Canceling Background Loading..."; 175 | m_imageLoadingFutureWatcher.cancel(); 176 | qDebug() << "Background Loading Canceled"; 177 | } 178 | 179 | QRect ImageListView::visualRect(const QModelIndex& index) const 180 | { 181 | if (!index.isValid()) { 182 | return QRect(); 183 | } 184 | // по строке модельного индекса вычисляем 185 | // строку фото 186 | int r = index.row() / m_columnCount; 187 | // колонку фото 188 | int c = index.row() % m_columnCount; 189 | // вычисляем ширину фото 190 | int width = viewport()->width() / m_columnCount; 191 | // вычисляем высоту фото 192 | int height = qMin(width, viewport()->height()); 193 | // получаем координаты фото в системе координат window 194 | int x = c * width; 195 | int y = r * height; 196 | // переводим в систему координат видового окна 197 | QRect result{ 198 | x - horizontalOffset(), 199 | y - verticalOffset(), 200 | width, 201 | height 202 | }; 203 | return result; 204 | } 205 | 206 | void ImageListView::scrollTo(const QModelIndex& index, ScrollHint hint) 207 | { 208 | Q_UNUSED(hint) 209 | 210 | QRect view = viewport()->rect(); 211 | QRect rect = visualRect(index); 212 | 213 | if (rect.top() < view.top()) { 214 | verticalScrollBar()->setValue(verticalScrollBar()->value() + rect.top() - view.top()); 215 | } else if (rect.bottom() > view.bottom()) { 216 | verticalScrollBar()->setValue( 217 | verticalScrollBar()->value() + qMin(rect.bottom() - view.bottom(), rect.top() - view.top())); 218 | } 219 | } 220 | 221 | QModelIndex ImageListView::indexAt(const QPoint& point) const 222 | { 223 | if (model()) { 224 | // point передан в системе координат viewport-a, поэтому 225 | // переводим координаты точки в систему координат window 226 | QPoint p{ point.x() + horizontalOffset(), point.y() + verticalOffset() }; 227 | // расчитываем ширину фото 228 | int width = viewport()->width() / m_columnCount; 229 | // расчитываем высоту фото 230 | int height = qMin(width, viewport()->height()); 231 | // расчитываем колонку фото 232 | int c = p.x() / width; 233 | // расчитываем строку фото 234 | int r = p.y() / height; 235 | // переводим в линейный индекс 236 | int i = r * m_columnCount + c; 237 | 238 | if (i >= 0 && i < model()->rowCount(rootIndex())) { 239 | return model()->index(i, 0, rootIndex()); 240 | } 241 | } 242 | return QModelIndex(); 243 | } 244 | 245 | QModelIndex ImageListView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) 246 | { 247 | Q_UNUSED(modifiers) 248 | 249 | QModelIndex index = currentIndex(); 250 | if (!index.isValid()) { 251 | return index; 252 | } 253 | int rowCount = model()->rowCount(rootIndex()); 254 | QRect viewRect = viewport()->rect(); 255 | int tileWidth = viewRect.width() / m_columnCount; 256 | int tileHeight = qMin(tileWidth, viewRect.height()); 257 | int viewColumnCount = viewRect.width() / tileWidth; 258 | int viewRowCount = viewRect.height() / tileHeight; 259 | int pageOffset = viewColumnCount * viewRowCount; 260 | 261 | int offset = 0; 262 | switch (cursorAction) { 263 | case MoveHome: 264 | offset = -index.row(); 265 | break; 266 | case MoveEnd: 267 | offset = qMax(rowCount - index.row() - 1, 0); 268 | break; 269 | case MovePageDown: 270 | offset += pageOffset; 271 | break; 272 | case MovePageUp: 273 | offset -= pageOffset; 274 | break; 275 | case MovePrevious: 276 | case MoveLeft: 277 | offset = -1; 278 | break; 279 | case MoveNext: 280 | case MoveRight: 281 | offset = 1; 282 | break; 283 | case MoveUp: 284 | if ((index.row() + 1) > m_columnCount) { 285 | offset = -m_columnCount; 286 | } 287 | break; 288 | case MoveDown: 289 | if ((index.row() + m_columnCount) < rowCount) { 290 | offset = +m_columnCount; 291 | } 292 | } 293 | return model()->index(qMax(0, qMin(index.row() + offset, rowCount - 1)), index.column(), rootIndex()); 294 | } 295 | 296 | int ImageListView::horizontalOffset() const 297 | { 298 | // у нас не будет скроллирования в горизонтальной плоскости 299 | return 0; 300 | } 301 | 302 | int ImageListView::verticalOffset() const 303 | { 304 | return verticalScrollBar()->value(); 305 | } 306 | 307 | bool ImageListView::isIndexHidden(const QModelIndex& index) const 308 | { 309 | Q_UNUSED(index); 310 | return false; 311 | } 312 | 313 | void ImageListView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command) 314 | { 315 | QPair rowRange = modelRowRangeForViewportRect(rect); 316 | QItemSelection selection; 317 | int begin = -1; 318 | int end = -1; 319 | for (int row = rowRange.first; row < rowRange.second; row++) { 320 | QModelIndex index = model()->index(row, 0, rootIndex()); 321 | QRect indexRect = visualRect(index); 322 | if (indexRect.intersects(rect)) { 323 | if (begin == -1) { 324 | begin = end = row; 325 | } else { 326 | if ((end + 1) == row) { 327 | end = row; 328 | } else { 329 | QModelIndex startIndex = model()->index(begin, 0, rootIndex()); 330 | QModelIndex finishIndex = model()->index(end, 0, rootIndex()); 331 | QItemSelection continuousSelection(startIndex, finishIndex); 332 | selection.merge(continuousSelection, command); 333 | begin = end = row; 334 | } 335 | } 336 | } 337 | } 338 | if (begin != -1) { 339 | QModelIndex startIndex = model()->index(begin, 0, rootIndex()); 340 | QModelIndex finishIndex = model()->index(end, 0, rootIndex()); 341 | QItemSelection continuousSelection(startIndex, finishIndex); 342 | selection.merge(continuousSelection, command); 343 | } 344 | selectionModel()->select(selection, command); 345 | } 346 | 347 | QRegion ImageListView::visualRegionForSelection(const QItemSelection& selection) const 348 | { 349 | QModelIndexList list = selection.indexes(); 350 | QRegion region; 351 | foreach (const QModelIndex& index, list) { 352 | QRect rect = visualRect(index); 353 | if (rect.isValid()) { 354 | region += rect; 355 | } 356 | } 357 | return region; 358 | } 359 | 360 | namespace { 361 | void paintOutline(QPainter& painter, const QRect& rect) 362 | { 363 | QRect r = rect.adjusted(1, 1, -1, -1); 364 | painter.save(); 365 | painter.drawRect(r); 366 | painter.restore(); 367 | } 368 | } 369 | 370 | void ImageListView::paintEvent(QPaintEvent* event) 371 | { 372 | m_invalidatingModelRows.clear(); 373 | Q_UNUSED(event); 374 | QList imageIndexList; 375 | QPair rowRange = modelRowRangeForViewportRect(event->rect()); 376 | for (int row = rowRange.first; row < rowRange.second; ++row) { 377 | imageIndexList.append(row); 378 | } 379 | QStylePainter painter(viewport()); 380 | painter.setRenderHints(QPainter::Antialiasing); 381 | 382 | foreach (int row, imageIndexList) { 383 | QModelIndex index = model()->index(row, 0, rootIndex()); 384 | if (!index.isValid()) { 385 | continue; 386 | } 387 | QRect rect = visualRect(index); 388 | if (!rect.isValid() || rect.bottom() < 0 || rect.y() > viewport()->height()) 389 | continue; 390 | QString imageFileName = model()->data(index).toString(); 391 | QImage* ptr = m_imageCache.object(imageFileName); 392 | if (ptr) { 393 | QImage* image = ptr; 394 | QRectF imageRect = image->rect(); 395 | QRectF drawRect = rect.adjusted(2, 2, -2, -2); 396 | if (imageRect.width() < imageRect.height()) { 397 | auto delta = (drawRect.width() - drawRect.width() * imageRect.width() / imageRect.height()) / 2.0; 398 | drawRect.adjust(delta, 0, -delta, 0); 399 | } else { 400 | auto delta = (drawRect.height() - drawRect.height() * imageRect.height() / imageRect.width()) / 2.0; 401 | drawRect.adjust(0, delta, 0, -delta); 402 | } 403 | painter.drawImage(drawRect, *image, imageRect, nullptr); 404 | } else { 405 | painter.setPen(QPen(QColor("gray"), 1)); 406 | painter.drawText(rect, Qt::AlignCenter, "Loading..."); 407 | } 408 | if (selectionModel()->isSelected(index)) { 409 | painter.setPen(QPen(QColor("red"), 1)); 410 | paintOutline(painter, rect); 411 | } else { 412 | if (currentIndex() == index) { 413 | painter.setPen(QPen(QColor("yellow"), 1)); 414 | paintOutline(painter, rect); 415 | } 416 | } 417 | } 418 | } 419 | 420 | void ImageListView::updateGeometries() 421 | { 422 | qDebug() << "Image List View updateGeometries called"; 423 | 424 | // получаем прямоугольник, описывающий окно просмотра 425 | QRect viewportRect = viewport()->rect(); 426 | // получаем ширину окна просмотра 427 | int viewportWidth = width(); 428 | // получаем ширину вертикальной полосы прокрутки 429 | int verticalScrollBarWidth = verticalScrollBar()->width(); 430 | // получаем количество строк модели 431 | int modelRowCount = model()->rowCount(rootIndex()); 432 | // расчитываем число строк в окне модели 433 | int windowRowCount = modelRowCount / m_columnCount + ((modelRowCount % m_columnCount) ? 1 : 0); 434 | // расчитываем ширину фото в видовом окне 435 | int imageWidth = viewportWidth / m_columnCount; 436 | // расчитываем высоту фото в видовом окне 437 | int imageHeight = qMin(imageWidth, viewportRect.height()); 438 | if (imageHeight) { 439 | int viewportRowCount = (viewportRect.height() / imageHeight + 1); 440 | m_imageCache.setMaxCost(viewportRowCount * m_columnCount * 2); 441 | } 442 | 443 | // если высоты вида недостаточна для показа модели целиком 444 | if (windowRowCount * imageHeight > viewportRect.height()) { 445 | // корректируем ширину окна просмотра, поскольку станет видима полоса прокрутки 446 | viewportWidth -= verticalScrollBarWidth; 447 | // расчитываем новый размер фото в видовом окне 448 | imageWidth = viewportWidth / m_columnCount; 449 | imageHeight = qMin(imageWidth, viewportRect.height()); 450 | // расчитываем максимальное смещение вертикальной полосы прокрутки с учетом корректировки 451 | int verticalScrollBarMaximum = windowRowCount * imageHeight; 452 | // если после корректировки высоты видового окна достаточно, чтобы вместить модель целиком 453 | if (verticalScrollBarMaximum < viewportRect.height()) { 454 | // оставляем один пиксель, чтобы полоса прокрутки осталась видима 455 | verticalScrollBarMaximum = 1; 456 | } else { 457 | // убираем одну страницу 458 | verticalScrollBarMaximum -= viewportRect.height(); 459 | } 460 | // настраиваем параметры вертикальной полосы прокрутки 461 | verticalScrollBar()->setRange(0, verticalScrollBarMaximum); 462 | verticalScrollBar()->setPageStep(viewportRect.height() / imageHeight * imageHeight); 463 | verticalScrollBar()->setSingleStep(imageHeight); 464 | 465 | } else { 466 | // окна просмотра достаточно, чтобы вместить модель целиком 467 | // поэтому скрываем вертикальную полосу прокрутки 468 | verticalScrollBar()->setRange(0, 0); 469 | } 470 | } 471 | 472 | void ImageListView::verticalScrollbarValueChanged(int value) 473 | { 474 | qDebug() << "Image List View verticalScrollbarValueChanged" << value << "called"; 475 | qDebug() << "verticalScrollbarValueChanged: before QAbstractItemView::verticalScrollbarValueChanged(value)"; 476 | QAbstractItemView::verticalScrollbarValueChanged(value); 477 | qDebug() << "verticalScrollbarValueChanged: end QAbstractItemView::verticalScrollbarValueChanged(value)"; 478 | startScrollDelayTimer(); 479 | } 480 | 481 | void ImageListView::resizeEvent(QResizeEvent* event) 482 | { 483 | qDebug() << "resizeEvent: before QAbstractItemView::resizeEvent(event)"; 484 | QAbstractItemView::resizeEvent(event); 485 | qDebug() << "resizeEvent: after QAbstractItemView::resizeEvent(event)"; 486 | startScrollDelayTimer(); 487 | 488 | qDebug() << "resizeEvent:" << width() << "" << height(); 489 | } 490 | 491 | void ImageListView::setModel(QAbstractItemModel* model) 492 | { 493 | qDebug() << "setModel: before QAbstractItemView::setModel(model)"; 494 | QAbstractItemView::setModel(model); 495 | qDebug() << "setModel: after QAbstractItemView::setModel(model)"; 496 | } 497 | 498 | void ImageListView::reset() 499 | { 500 | qDebug() << "Image List View reset called"; 501 | m_imageCache.clear(); 502 | m_invalidatingModelRows.clear(); 503 | qDebug() << "reset: before QAbstractItemView::reset()"; 504 | QAbstractItemView::reset(); 505 | qDebug() << "reset: after QAbstractItemView::reset()"; 506 | startScrollDelayTimer(); 507 | } 508 | -------------------------------------------------------------------------------- /imagelistview.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGELISTVIEW_H 2 | #define IMAGELISTVIEW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | class QTimer; 14 | 15 | /** 16 | * @brief The ImageLoadingTask struct 17 | * Вспомогательная структура для фоновой загрузки 18 | */ 19 | struct ImageLoadingTask { 20 | int row; 21 | QString imageFileName; 22 | std::unique_ptr image; 23 | }; 24 | using ImageLoadingTaskSharedPtr = std::shared_ptr; 25 | using ImageLoadingTaskFutureWatcher = QFutureWatcher; 26 | 27 | /** 28 | * @brief The ImageListView class 29 | * ImageListView - класс вида списка изображений 30 | */ 31 | class ImageListView : public QAbstractItemView { 32 | Q_OBJECT 33 | public: 34 | ImageListView(QWidget* parent = Q_NULLPTR); 35 | 36 | // ImageListView interface 37 | public: 38 | /** 39 | * @brief columnCount возвращает число колонок вида 40 | * @return число колонок вида 41 | */ 42 | int columnCount() const; 43 | /** 44 | * @brief setColumnCount устанавливает число колонок вида 45 | * @param columnCount новое число колонок 46 | */ 47 | void setColumnCount(int columnCount); 48 | 49 | protected: 50 | /** 51 | * @brief modelIndexRangeForRect возвращает полуоткрытый диапазон модельных строк, 52 | * попадающих в для заданную прямоугольную область rect 53 | * @param rect 54 | * @return полуотркрытый диапазон модельных строк (model index row) 55 | */ 56 | QPair modelRowRangeForViewportRect(const QRect& rect); 57 | /** 58 | * @brief startScrollDelayTimer запускает таймер отсрочки скрола 59 | */ 60 | void startScrollDelayTimer(); 61 | void stopScrollDelayTimer(); 62 | void startAsyncImageLoading(); 63 | void stopAsyncImageLoading(); 64 | 65 | // QAbstractItemView interface 66 | public: 67 | virtual QRect visualRect(const QModelIndex& index) const override; 68 | virtual QModelIndex indexAt(const QPoint& point) const override; 69 | virtual void scrollTo(const QModelIndex& index, ScrollHint hint) override; 70 | virtual void setModel(QAbstractItemModel* model) override; 71 | 72 | public slots: 73 | virtual void reset() override; 74 | 75 | protected: 76 | virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; 77 | virtual int horizontalOffset() const override; 78 | virtual int verticalOffset() const override; 79 | virtual bool isIndexHidden(const QModelIndex& index) const override; 80 | virtual void setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command) override; 81 | virtual QRegion visualRegionForSelection(const QItemSelection& selection) const override; 82 | 83 | protected slots: 84 | virtual void updateGeometries() override; 85 | virtual void verticalScrollbarValueChanged(int value) override; 86 | 87 | // QWidget interface 88 | protected: 89 | virtual void paintEvent(QPaintEvent* event) override; 90 | virtual void resizeEvent(QResizeEvent* event) override; 91 | 92 | // State 93 | private: 94 | /** 95 | * @brief m_columnCount число колонок изображений 96 | */ 97 | int m_columnCount = 5; 98 | /** 99 | * @brief m_loadingDelayTimer таймер отсроченной реакции на скроллирование 100 | */ 101 | QTimer* m_loadingDelayTimer = nullptr; 102 | /** 103 | * @brief m_updateDelayTimer таймер отсроченной реакции на обновление вида 104 | */ 105 | QTimer* m_updatingDelayTimer = nullptr; 106 | /** 107 | * @brief m_imageLoadingFutureWatcher наблюдатель за фоновой загрузкой 108 | */ 109 | ImageLoadingTaskFutureWatcher m_imageLoadingFutureWatcher; 110 | /** 111 | * @brief m_invalidatingModelRows список инвалидируемых строк модели 112 | */ 113 | QList m_invalidatingModelRows; 114 | /** 115 | * @brief m_imageCache кеш изображений фиксированного размера 116 | */ 117 | QCache m_imageCache; 118 | }; 119 | 120 | #endif // IMAGELISTVIEW_H 121 | -------------------------------------------------------------------------------- /imageviewer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatoly-spb/qtimageviewer/eb10c45a6636d48b22f31f4ec9e887d3a63af8e2/imageviewer.ico -------------------------------------------------------------------------------- /imageviewer.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2018-01-22T23:41:18 4 | # 5 | #------------------------------------------------- 6 | 7 | QT += core gui concurrent 8 | 9 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 | 11 | #INCLUDEPATH += $$(RXCPP_HOME)/rx/v2/src 12 | TARGET = imageviewer 13 | TEMPLATE = app 14 | RC_FILE = imageviewer.rc 15 | 16 | # The following define makes your compiler emit warnings if you use 17 | # any feature of Qt which has been marked as deprecated (the exact warnings 18 | # depend on your compiler). Please consult the documentation of the 19 | # deprecated API in order to know how to port your code away from it. 20 | DEFINES += QT_DEPRECATED_WARNINGS 21 | 22 | # You can also make your code fail to compile if you use deprecated APIs. 23 | # In order to do so, uncomment the following line. 24 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 25 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 26 | 27 | 28 | SOURCES += \ 29 | main.cpp \ 30 | mainwindow.cpp \ 31 | imagelistmodel.cpp \ 32 | imagelistview.cpp 33 | 34 | HEADERS += \ 35 | mainwindow.h \ 36 | imagelistmodel.h \ 37 | imagelistview.h \ 38 | 39 | FORMS += \ 40 | mainwindow.ui 41 | 42 | RESOURCES += \ 43 | imageviewer.qrc 44 | -------------------------------------------------------------------------------- /imageviewer.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | three.png 4 | two.png 5 | 6 | 7 | -------------------------------------------------------------------------------- /imageviewer.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "imageviewer.ico" 2 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QApplication a(argc, argv); 7 | MainWindow w; 8 | w.show(); 9 | 10 | return a.exec(); 11 | } 12 | -------------------------------------------------------------------------------- /mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "imagelistmodel.h" 3 | #include "ui_mainwindow.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | MainWindow::MainWindow(QWidget* parent) 10 | : QMainWindow(parent) 11 | , ui(new Ui::MainWindow) 12 | { 13 | ui->setupUi(this); 14 | 15 | imageListModel = new ImageListModel{ this }; 16 | fileSystemModel = new QFileSystemModel{ this }; 17 | fileSystemModel->setFilter(QDir::Dirs | QDir::NoDotAndDotDot); 18 | fileSystemModel->setRootPath(""); 19 | ui->treeView->setModel(fileSystemModel); 20 | for (int hiddenColumn = fileSystemModel->columnCount(); hiddenColumn > 1; --hiddenColumn) { 21 | ui->treeView->hideColumn(hiddenColumn - 1); 22 | } 23 | ui->listView->setColumnCount(3); 24 | ui->actionThree_Columns->setChecked(true); 25 | ui->listView->setModel(imageListModel); 26 | } 27 | 28 | MainWindow::~MainWindow() 29 | { 30 | delete ui; 31 | } 32 | 33 | void MainWindow::changeEvent(QEvent* e) 34 | { 35 | QMainWindow::changeEvent(e); 36 | switch (e->type()) { 37 | case QEvent::LanguageChange: 38 | ui->retranslateUi(this); 39 | break; 40 | default: 41 | break; 42 | } 43 | } 44 | 45 | void MainWindow::on_treeView_clicked(const QModelIndex& index) 46 | { 47 | QFileInfo fileInfo = fileSystemModel->fileInfo(index); 48 | qDebug() << "New folder " << fileInfo.absoluteFilePath() << "has been selected"; 49 | if (fileInfo.isDir()) { 50 | imageListModel->loadDirectoryImageList(fileInfo.absoluteFilePath()); 51 | } 52 | } 53 | 54 | void MainWindow::on_actionTwo_Columns_triggered() 55 | { 56 | ui->actionTwo_Columns->setChecked(true); 57 | ui->actionThree_Columns->setChecked(false); 58 | ui->listView->setColumnCount(2); 59 | } 60 | 61 | void MainWindow::on_actionThree_Columns_triggered() 62 | { 63 | ui->actionTwo_Columns->setChecked(false); 64 | ui->actionThree_Columns->setChecked(true); 65 | ui->listView->setColumnCount(3); 66 | } 67 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class MainWindow; 8 | } 9 | 10 | class QFileSystemModel; 11 | class ImageListModel; 12 | 13 | class MainWindow : public QMainWindow { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit MainWindow(QWidget* parent = Q_NULLPTR); 18 | ~MainWindow(); 19 | 20 | protected: 21 | void changeEvent(QEvent* e); 22 | 23 | private slots: 24 | void on_treeView_clicked(const QModelIndex& index); 25 | 26 | void on_actionTwo_Columns_triggered(); 27 | 28 | void on_actionThree_Columns_triggered(); 29 | 30 | private: 31 | Ui::MainWindow* ui; 32 | QFileSystemModel* fileSystemModel; 33 | ImageListModel* imageListModel; 34 | }; 35 | 36 | #endif // MAINWINDOW_H 37 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 604 10 | 493 11 | 12 | 13 | 14 | Image Viewer 15 | 16 | 17 | 18 | 19 | 20 | 21 | Qt::Horizontal 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 0 33 | 0 34 | 604 35 | 26 36 | 37 | 38 | 39 | 40 | 41 | Qt::Horizontal 42 | 43 | 44 | true 45 | 46 | 47 | TopToolBarArea 48 | 49 | 50 | false 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | true 59 | 60 | 61 | 62 | :/icons/two.png:/icons/two.png 63 | 64 | 65 | Two Columns 66 | 67 | 68 | 69 | 70 | true 71 | 72 | 73 | 74 | :/icons/three.png:/icons/three.png 75 | 76 | 77 | Three Columns 78 | 79 | 80 | 81 | 82 | 83 | 84 | ImageListView 85 | QListView 86 |
imagelistview.h
87 |
88 |
89 | 90 | 91 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatoly-spb/qtimageviewer/eb10c45a6636d48b22f31f4ec9e887d3a63af8e2/three.png -------------------------------------------------------------------------------- /two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anatoly-spb/qtimageviewer/eb10c45a6636d48b22f31f4ec9e887d3a63af8e2/two.png --------------------------------------------------------------------------------