实现ControlPanelForm

ControlPanelForm用于显示界面左侧的信息栏,它有两部分组成:上面主要是一个QLabel,用于显示样本的预览图,下方是一个QTableWidget,用于显示扫描进度。是一个很粗糙的界面设计,但是用于演示也够了。

它包含四个slot函数,我们会依次简单看一下:

显示预览图信息

函数onSigPreviewCaptured()用于在收到sigPreviewed后显示样本信息。它将收到的cv::Mat转换成QPixmap,并按照这个QLabel的实际大小做缩放,并设置到QLabel上面。

1
2
3
4
5
6
7
8
9
10
void ControlPanelForm::onSigPreviewCaptured(cv::Mat prev_mat, cv::Mat label_mat, const QString &qrcode)
{
_impl->_prevMat = prev_mat.clone();
_impl->_qrcode = qrcode;

auto pixmap = ImageTool::MatToQPixmap(prev_mat).scaled(ui->previewImage->size(), Qt::KeepAspectRatio);
ui->previewImage->setPixmap(pixmap);
ui->previewImage->setEnabled(true);
ui->previewLabel->setText(qrcode);
}

绘制扫描范围

因为在ScanTask中是将预览图和扫描范围分两次发送的,所以这里也需要提供两个slot实现。它也很简单,也就是将传过来的矩形范围画到预览图上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ControlPanelForm::onSigLowScanRanged(QRect rect)
{
void ControlPanelForm::onSigLowScanRanged(QRect rect)
{
RETURN_LOG_IF(_impl->_prevMat.empty(), "保存的预览图无效!");
RETURN_LOG_IF(!rect.isValid(), "无效的扫描区域!");

QPixmap thumbnail = ImageTool::MatToQPixmap(_impl->_prevMat).scaled(ui->previewImage->size(), Qt::KeepAspectRatio);
QPainter painter(&thumbnail);
painter.setPen(QPen((QColor(255,0,0, 180)), 2));
qreal ratio = thumbnail.width() / qreal(_impl->_prevMat.cols);
painter.drawRect(QRect(rect.topLeft()*ratio, rect.size()*ratio));
ui->previewImage->setPixmap(thumbnail);
}
}

绘制扫描进度

ScanTask通过两条信号来通知扫描进度信息:sigScanStart()在启动扫描后发出,通知要扫描的照片的数量,而sigImageCaptured()在采集到每张照片后发出,会携带这张照片的位置以及图像内容。我们会根据sigScanStart()传过来的扫描的行列号计算表格的行列尺寸,并在收到sigImageCaptured()后将代表这个视野的图片涂成绿色。我们只是处理了了连续扫描的情况,对于非连续扫描,传过来的columns会是扫描视野数,rows会是1,如何调整显示就不在这里讨论了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void ControlPanelForm::onSigScanStart(int columns, int rows, int method)
{
auto container = ui->tableWidget->parentWidget();
if(method!=EScanMethod::eMatrixScan)
{
// 非连续扫描的处理, 修改columns和rows的值
// ....
}
int width = container->width();
int height = qRound((qreal)width * rows / columns);
if(height + ui->tableWidget->y() > container->height())
{
height = container->height() - ui->tableWidget->y();
width = qRound(height * columns / qreal(rows));
container->layout()->setAlignment(ui->tableWidget, Qt::AlignHCenter);
}

ui->tableWidget->setRowCount(rows);
ui->tableWidget->setColumnCount(columns);
ui->tableWidget->setFixedSize(width, height);
container->layout()->update();
}

void ControlPanelForm::onSigImageCaptured(const QPoint &pos, cv::Mat image)
{
Q_UNUSED(image)
RETURN_LOG_IF(pos.x()<0||pos.y()<0, QString("pos值(%1,%2)非法!").arg(pos.x()).arg(pos.y()));

auto item = new QTableWidgetItem;
item->setBackground(Qt::green);
ui->tableWidget->setItem(pos.y(), pos.x(), item);
}

实现MainClientForm

MainClientForm是软件的主客户区域,它的主体是一个QGraphicsView,用于显示拍摄的照片。
在Qt中显示图像,最常用的是QLabel,就如我们在显示预览图时所作的。还有一种做法是使用QGraphicsView框架,利用QGraphicsPixmapItem来显示图片。

QGraphicsView框架,据说最初的开发初衷是用于QtQuick里面的,但是后面却不知什么原因没有出现,据说是被废弃了。但是,我认为,这个框架是Qt的图像机制中最有价值的东西,这么说,QtWidgetQQuick,去掉了GraphicsView之后剩下的东西,和WPF比起来,说垃圾有点过分,但也好不到哪里去,最多只是一个弱化版的WPF。但是有了GraphicsView框架,就不一样了,在很多场景下,这是不可替代的。不过,现在客户端都在退化,我们看到,微软在WPF之后推出的一代代GUI框架,在功能上相对于WPF也是在不断弱化中。因为移动端不需要也做不了太复杂的功能。在Qt中,利用GraphicsView框架,可以将整个程序界面建立在上面。有兴趣的人可以去看一下Qt的文档和例子。

在真实的产品中,我们需要对显示的图像做一些处理和计算,显示一些信息,在这里,我们只是简单显示一下图像就可以了。我们不会对ViewScene有什么特别的要求,所以简单地使用Qt提供的原生类就可以了,我们也仅仅需要显示一个QGraphicsPixmapItem,所以连Scene的实例都不需要保存,直接在构造函数中创建并设置给View就行了。在后面,我们会使用更复杂的Scene/View,这次权做热身使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct MainClientForm::Implementation
{
QGraphicsPixmapItem *_photoItem;
};

MainClientForm::MainClientForm(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MainClientForm)
, _impl{new Implementation}
{
ui->setupUi(this);
ui->photoView->setScene(new QGraphicsScene(0,0,2448,2048, this));
_impl->_photoItem = ui->photoView->scene()->addPixmap(QPixmap());
}

void MainClientForm::resizeEvent(QResizeEvent *event)
{
ui->photoView->fitInView(ui->photoView->sceneRect(), Qt::KeepAspectRatio);
QWidget::resizeEvent(event);
}

void MainClientForm::onSigImageCaptured(const QPoint &pos, cv::Mat image)
{
_impl->_photoItem->setPixmap(ImageTool::MatToQPixmap(image) );
}

我们重载了resizeEvent(),在里面更新了View的大小。这大概是最简单的让显示图像自动缩放适应窗口大小的做法。

接下来实现用户交互。

实现onSigChooseOptions()函数

我们在前面开发ScanTask::verifyOptions()的时候介绍过,通过发送信号sigChooseOptions,通知界面显示参数确认对话框:

1
2
3
4
5
6
7
8
QVariantHash params{
...
};
bool ok = true;
emit sigChooseOptions(params, ok);
if(ok)
{
...

ScannerMainWindow::onActSingleScan()中,使用Qt::BlockingQueuedConnection方式将sigChooseOptionsScannerMainWindow::onSigChooseOptions关联。

我们先创建对话框,并指定类名为SingleScanOptionDialog,我们使用它来显示单张扫描时的扫描参数。

对话框的实现本身没有什么值得说的,唯一要注意的是实现的时候要注意和ScanTask相对照,以避免QVariantMap出现key的错误。

ScannerMainWindow::onSigChooseOptions()的实现则很简单:显示对话框,并将结果保存在optionsok中返回给调用者:

1
2
3
4
5
6
7
8
9
10
11
12
13
void ScannerMainWindow::onSigChooseOptions(QVariantHash &options, bool &ok)
{
SingleOptionDialog dlg;
dlg.setParam(options);
if(dlg.exec() != QDialog::Accepted)
{
ok = false;
return ;
}
ok = true;
options = dlg.params();
return;
}