1、简述
本博客主要记载开发过程中的不熟悉的Qt小知识点。本软件是上海火克公司开发的一款应用软件,软件实现的功能主要有三个部分,1是项目管理,2是计算管道覆盖比,3是自动生成word报告。
2、内容记录
2.1 QStatusBar的使用
效果图:
代码:
// Initial the statusBar
ui.statusBar->setStyleSheet(QString("QStatusBar::item{border: 0px}"));
QLabel* label0 = new QLabel("User: ");
label0->setAlignment(Qt::AlignLeft);
ui.statusBar->addPermanentWidget(label0,0); // 第二个参数为所占比例
m_qUserName = new QLabel;
m_qUserName->setAlignment(Qt::AlignLeft);
ui.statusBar->addPermanentWidget(m_qUserName,5);
QLabel* label1 = new QLabel("mm");
ui.statusBar->addPermanentWidget(label1,1);
2.1.1 QAction
说明:由于在使用QMenu的时候,发现其不能如同按钮一样,百思不得其解。最后请教师兄,师兄教我直接对statusBar添加QAction,槽函数响应triggered()信号。而且经过测试,发现QMenu并不具有triggered()信号。
代码:
QAction *pProjects = new QAction("项目",nullptr);
QAction *pCustomers = new QAction("客户", nullptr);
QAction *pTemplates = new QAction("模板", nullptr);
QAction *pVersion = new QAction("版本", nullptr);
QAction *pAbout = new QAction("关于", nullptr);
ui.menuBar->addAction(pProjects);
ui.menuBar->addAction(pCustomers);
ui.menuBar->addAction(pTemplates);
ui.menuBar->addAction(pVersion);
ui.menuBar->addAction(pAbout);
connect(pCustomers, SIGNAL(triggered()), this, SLOT(OnBtCustomClicked()));
connect(pTemplates, SIGNAL(triggered()), this, SLOT(OnBtTemplateClicked()));
2.2 设置弹窗的模态
功能:阻止父窗口
代码:
// m_pAddName是widget类型的子窗口
m_pAddName->setWindowFlags( this->windowFlags() | Qt::Dialog);
m_pAddName->setWindowModality(Qt::ApplicationModal); // 设置为模态
2.3 获取计算机用户名
代码:
QString userName = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
userName = userName.section("/", -1, -1);
2.4 QDoubleSpinBox的响应事件
功能:实现QDoubleSpinBox的键盘修改响应事件和点击增加减少的响应事件
代码:
ui.m_dsbWidth->setKeyboardTracking(false); // 键入数据失去焦点后才响应,如不添加,则一变就响应
connect(ui.m_dsbWidth, SIGNAL(valueChanged(double)), this, SLOT(onDSBWidthValueChanged(double)));
ui.m_dsbHeight->setKeyboardTracking(false);
2.5 QPainter
移动坐标轴,缩放坐标系,使作图区域填满并位于frame的中心。
代码:
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(iCanvasWidth / 2 + dCanvasX, iCanvasHeight / 2 + dCanvasY); // 将原点设为frame的中心
double dEdgeWidth, dEdgeHeight; // 矩形边框
double scale; // 坐标轴缩放因子
dEdgeWidth = dShapeWidth + 40;
dEdgeHeight = dShapeHeight + 40;
double scaleWidth = (iCanvasWidth - 20) / dEdgeWidth;
double scaleHeight = (iCanvasHeight - 20) / dEdgeHeight;
scale = std::min(scaleWidth, scaleHeight);
painter.scale(scale, scale); // 缩放坐标系
2.6 QPainterPath
画槽形环
效果图:
代码:
QPainterPath path;
path.addRoundedRect(-dShapeWidth / 2, -dShapeHeight / 2, dShapeWidth,
dShapeHeight, dShapeRadius, dShapeRadius); // 外轮廓
path.addRoundedRect(-dShapeWidth / 2 - 10, -dShapeHeight / 2 - 10, dShapeWidth + 20,
dShapeHeight + 20, dShapeRadius + 10, dShapeRadius + 10); // 内轮廓
painter.fillPath(path, Qt::red);
2.7 圆心定位算法
该算法是一道数学难题,很难得到一个通用的圆心坐标公式。经过与导师讨论,想到另外一种解决问题的方法。
解题思路:利用图像处理的方法遍历确定圆心
1.创建一个Mat类的灰度图,初始化为255。
2.给矩形槽区域外的像素赋值0。0像素代表不合理的位置,或者已存在圆。
3.由下向上遍历,并同时由中间向两边遍历。以(i,j)为中心的圆形区域,若没有0像素,则记录该位置。
由此方式可近似确定圆心坐标,其中精度由实际尺寸与像素比决定,即分辨率决定。
贴出一张结果图:输出圆心坐标,bool类型的是否溢出:0-溢出 1-未溢出
源码:
calcularCenter.h
#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
using namespace cv;
using namespace std;
struct CableData
{
int iAmount;
double dOutDiameter;
};
// 获取电缆圆心坐标
bool GetPoints(const vector<CableData>& vData, vector<Point2f>& vPoints, double dWidth, double dHeight, double dRadius);
// 添加一个圆,传出圆心坐标
bool AddCircle(Mat& image, Point2i &point, const int& radius);
// 将一个添加的圆涂黑
void PaintBlack(Mat& image, const Point2i& point, const int& diameter);
// 圆心在point的圆是否存在像素为0的点,true:没有像素为0的点,false:有像素为0的点
bool IsContain0(const Mat& image, const Point2i& point, const int& diameter);
// 计算2点之间距离的平方
int GetDistance(const Point2i& p1, const Point2i& p2);
calcularCenter.cpp
#include "calcularCenter.h"
bool GetPoints(const vector<CableData>& vData, vector<Point2f>& vPoints, double dWidth, double dHeight, double dRadius)
{
double scale;
if (dWidth > dHeight)
scale = 1000 / dWidth;
else
scale = 1000 / dHeight;
int nRow = ceil(dHeight * scale);
int nCol = ceil(dWidth * scale);
int nRadius = ceil(dRadius * scale);
int nRadius_2 = ceil(dRadius * scale * dRadius * scale);
Mat src(nRow,nCol,CV_8UC1,Scalar::all(255));
if (!src.data)
{
cout << "error!" << endl;
return false;
}
//将圆角矩形外面涂黑
for(int i = 0; i < nRow; i++)
for (int j = 0; j < nCol; j++)
{
if (i < nRadius && j < nRadius && GetDistance(Point2i(j, i), Point2i(nRadius, nRadius)) > nRadius_2)
{
src.data[i*nCol + j] = 0;
}
else if (i > nRow - nRadius && j < nRadius && GetDistance(Point2i(j, i), Point2i(nRadius, nRow - nRadius - 1)) > nRadius_2)
{
src.data[i*nCol + j] = 0;
}
else if (i < nRadius && j > nCol - nRadius && GetDistance(Point2i(j, i), Point2i(nCol - nRadius - 1, nRadius)) > nRadius_2)
{
src.data[i*nCol + j] = 0;
}
else if (i > nRow - nRadius && j > nCol - nRadius && GetDistance(Point2i(j, i), Point2i(nCol - nRadius - 1, nRow - nRadius - 1)) > nRadius_2)
{
src.data[i*nCol + j] = 0;
}
else
{
}
}
bool flag = true;
// 逐个添加圆
int size = vData.size();
int k;
for (k = size - 1; k >= 0; k--)
{
int radius = floor(vData[k].dOutDiameter * scale / 2);
for (int m = 0; m < vData[k].iAmount; m++ )
{
Point2i point;
if (AddCircle(src, point, radius))
{
Point2f _point;
_point.x = point.x / scale - dWidth / 2;
_point.y = point.y / scale - dHeight / 2;
vPoints.push_back(_point);
}
else
flag = false;
}
}
imshow("src",src);
imwrite("1.bmp", src);
waitKey(0);
return flag;
}
/****************************************************************************
* 函数:AddCircle
* 功能:添加一个圆,成功返回true; 不成功,则表示溢出,返回false.传出圆心坐标
* 参数:image,已确定圆的图像,point,传出的圆心坐标, radius,添加圆的半径
* 返回值:bool
****************************************************************************/
bool AddCircle(Mat & image, Point2i& point, const int & radius)
{
int nRow = image.rows;
int nCol = image.cols;
for (int i = nRow - 1; i > 0; i--)
{
for (int j = nCol / 2 - 1; j > 0; j--)
{
if (IsContain0(image, Point2i(j, i), radius))
{
PaintBlack(image, Point2i(j, i), radius);
point = Point2i(j, i);
return true;
}
}
for (int j = nCol / 2; j < nCol; j++)
{
if (IsContain0(image, Point2i(j, i), radius))
{
PaintBlack(image, Point2i(j, i), radius);
point = Point2i(j, i);
return true;
}
}
}
return false;
}
/****************************************************************************
* 函数:PaintBlack
* 功能:将添加的圆涂黑
* 参数:image,被修改的图片;point,圆心坐标;radius,半径大小
* 返回值:void
****************************************************************************/
void PaintBlack(Mat & image, const Point2i & point, const int & radius)
{
int nRow = image.rows;
int nCol = image.cols;
int iR_2 = radius * radius;
for (int i = point.y + radius; i >= point.y - radius; i--)
for (int j = point.x + radius; j >= point.x - radius; j--)
{
if (GetDistance(Point2i(j, i), point) <= iR_2)
{
image.data[i*nCol + j] = 0;
}
}
}
/****************************************************************************
* 函数:IsContain0
* 功能:判断圆心在该点的圆是否相交
* 参数:image,已确定的圆的图像; point,圆心; radius,半径大小
* 返回值:bool
****************************************************************************/
bool IsContain0(const Mat & image, const Point2i & point, const int & radius)
{
int nRow = image.rows;
int nCol = image.cols;
int iR_2 = radius * radius;
if (point.x - radius < 0 || point.x + radius > nCol - 1
|| point.y - radius < 0 || point.y + radius > nRow - 1)
return false;
for (int i = point.y + radius; i >= point.y - radius; i--)
for (int j = point.x + radius; j >= point.x - radius; j--)
{
if (GetDistance(Point2i(j, i), point) <= iR_2)
{
if (image.data[i*nCol + j] == 0)
return false;
}
}
return true;
}
/****************************************************************************
* 函数:GetDistance
* 功能:计算2点距离
* 参数:p1,p2像素坐标
* 返回值:double
****************************************************************************/
int GetDistance(const Point2i & p1, const Point2i & p2)
{
return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
}
main.cpp
#include "calcularCenter.h"
int main()
{
vector<CableData> vCableData;
vector<Point2f> vPoints;
vector<Point2f> vPoints1;
bool flag;
CableData data;
data.iAmount = 3;
data.dOutDiameter = 5;
vCableData.push_back(data);
data.iAmount = 3;
data.dOutDiameter = 20;
vCableData.push_back(data);
flag = GetPoints(vCableData, vPoints, 100, 100, 50);
for (int i = 0; i < vPoints.size(); i++)
{
cout << "(" << vPoints[i].x << ", " << vPoints[i].y << ")" << endl;
}
cout << int(flag)<< endl;
system("pause");
return 0;
}
反思:
由于功能需要,在已插入小圆后,需要求用标准直径的小圆填充大圆的小圆个数。最初使用的方法是,直接在getPoints函数后面添加对溢出的判断,若未溢出,则对标准直接的圆addCircle(),nFiller数字为记录的数量。代码如下:
// 逐个添加圆
for (int k = size - 1; k >= 0; k--)
{
int tmp = GetSleeveDiameter(vData[k].dOutDiameter);
int radius = floor(tmp * scale / 2);
for (int m = 0; m < vData[k].iAmount; m++)
{
Point2i point;
if (AddCircle(src, point, radius))
{
QPointF _point;
_point.rx() = point.x / scale - dWidth / 2;
_point.ry() = point.y / scale - dHeight / 2;
vPoints.push_back(_point);
vDiameter.push_back(vData[k].dOutDiameter);
}
else
flag = false;
}
}
// add filler sleeves
//if (flag)
//{
// Point2i point;
// int radius = floor(27 * scale / 2);
// while (AddCircle(src,point, radius))
// {
// nFiller[0]++;
// }
// radius = floor(19 * scale / 2);
// while (AddCircle(src, point, radius))
// {
// nFiller[1]++;
// }
// //radius = floor(12 * scale / 2);
// //while (AddCircle(src, point, radius))
// //{
// // nFiller[2]++;
// //}
//}
其结果导致每一次刷新耗费大量事件计算十分卡顿。
总结
由于paintEvent()是一个不断重复执行的函数,如果每一次主要数据没有变化的情况下paintEvent()事件响应,都得执行该圆心的求解,十分耗时,界面卡顿。因此,要考虑在数据没有变化的情况下,直接利用上一次计算的结果,而不用重复计算。同理,对于求填充填充圆的个数时,这个数据只有在特定情况下才需要(本项目是在响应按钮save和report时),不必要对每次数据变化计算该值。最后解决的办法时将GetPoints函数中的Mat类型作为一个全局变量,在按钮save和report的槽函数添加计算该值的代码。如下:
// add filler sleeves
int a[2] = { 0 };
if (m_isFill)
{
Point2i point;
int radius = floor(27 * scale / 2);
while (AddCircle(src,point, radius))
{
a[0]++;
}
radius = floor(19 * scale / 2);
while (AddCircle(src, point, radius))
{
a[1]++;
}
}
m_penetration.iFillerSleeve27Amount = a[0];
m_penetration.iFillerSleeve19Amount = a[1];
将 PaintBlack()替换为opencv自带的函数 circle(image, Point2i(j, i), radius, Scalar(0), -1);
2.8 排序输出
在项目中,得到一个vector<struct>数据,struct包含多种数据,要按照A方式输出,当A相同时,按照B输出。为了实现该功能,我采用Map<int, Map<String, int>>的数据结构,将数据改造成该种类型,利用map对key的排序,然后遍历map输出数据。
部分代码:
QMap<int, QMap<QString, int>> mapInsertSleeves;
int sizeOfPene = m_qvPenetrations.size();
for (int i = 0; i < sizeOfPene; i++)
{
if (!mapInsertSleeves.contains(m_qvPenetrations[i].iFillerSleeve19Length))
{
mapInsertSleeves[m_qvPenetrations[i].iFillerSleeve19Length] = QMap<QString, int>();
}
QMap<QString, int> &_map = mapInsertSleeves[m_qvPenetrations[i].iFillerSleeve19Length];
int sizeOfCable = m_qvPenetrations[i].qvCables.size();
QString str;
for (int j = 0; j < sizeOfCable; j++)
{
int d = PenetrationDetails::GetSleeveDiameter(m_qvPenetrations[i].qvCables[j].dOutDiameter);
if (d == 12)
str = "D12/6";
else if (d == 15)
str = "D15/8";
else if (d == 17)
str = "D17/10";
else if (d == 19)
str = "D19/12";
else if (d == 21)
str = "D21/14";
else if (d == 23)
str = "D23/16";
else if (d == 27)
str = "D27/19";
else if (d == 31)
str = "D31/23";
else if (d == 35)
str = "D35/27";
else if (d == 41)
str = "D41/31";
else if (d == 46)
str = "D46/36";
else if (d == 52)
str = "D52/42";
else if (d == 58)
str = "D58/48";
else if (d == 64)
str = "D64/54";
else
str = "D70/60";
if (_map.contains(str))
{
_map[str] += m_qvPenetrations[i].qvCables[j].iAmount;
}
else
{
_map[str] = m_qvPenetrations[i].qvCables[j].iAmount;
}
}
}
回顾:
昨天学习数据结构与算法一书时,无意中在某博客看到:
priority_queue<pair<int, int>, vector<pair<int, int>>, greater(pair<int, int>)>的结构,在first相等时,可以按second排序,可以应用到此处。
2.9 QFileDialog::getSaveFileName
保存文件到桌面,并设置默认的文件名。代码:
QString defaultSaveName = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "./" + m_penetration.qstrPenetration + ".doc";
QString outFileName = QFileDialog::getSaveFileName(this, QStringLiteral("请输入要保存的名字:"), defaultSaveName, "Microsoft Word 97-2003(*.doc);;Microsoft Word 2007-2013(*.docx)");
if (outFileName.isEmpty()) {
QMessageBox::warning(this, tr("警告"), tr("输入的文件名为空!"), QMessageBox::Ok);
return;
}
Report(outFileName);
2.10 Qt 输出word
使用封装好的源码QWordDemo.zip,源码出处已遗忘,以后若想起补上。