关于QT的系统总结

 

编译环境与开发流程

开发QT有两种IDE可以使用,一种是使用 VS + Qt 的插件,另一种就是使用QtCreator工具。前一种是微软的工具,用的都比较多容易上手,缺点是信号槽的支持不太好,需要手写,不能自动生成,另外可能有中文编码的问题。后一种是Qt的官方IDE,智能提示与调试功能不如VS强大,但是是跨平台的IDE,其QtDesigner设计UI界面操作比较方便,并且由于是QT官方的IDE,对编码等支持都比较好,里面集成了Qt的帮助文档。不得不说Qt的帮助文档做的是非常好的,集成进QtCreator环境之后更加方便。

我开发的时候使用的是QtCreator开发,目前除了调试功能比VS差以外,其他的用的比较顺手,QtCreator是跨平台的,ubuntu上也是可以使用,打开之后界面如下:
11

下面将对QtCreator的界面各个功能进行大致的介绍:
1

我们建立一个示例项目,选择“文件”—“新建文件或项目”—“应用程序”—“QT Widgets Application”选择之后都选择默认设置,根据提示,就得到了一个项目,我们的UI是一个基于QMainWindow的类,默认提供菜单栏,状态栏。如果不需要这些,可以建立一个基于QWidget的UI类,项目如图所示:
1

QT项目的构成及原理

将项目切换到编辑模式,如下:
1

这个项目中一共有4个文件,入口文件main.cpp、mainwindow.ui文件、mainwindow.h和mainwindow.cpp后台源文件,在main函数中直接调用MainWindow类的show()方法显示主界面,那么我们切换到UI的设计视图(双击项目中的mainwindow.ui文件),在主界面上添加两个控件:

1

我们看一下MainWindow.cpp的代码里面应该如何操作界面上的控件:
1

我们使用的是ui->txtName->text();这样的语句,也就是说并不是像在C#中一样在后台代码中直接可以通过类似this->txtName->text()的语句去访问界面上的控件对象,而MainWindow类中有一个成员变量是ui,其类型是Ui::MainWindow,通过这个ui成员去访问界面上的元素,那么这些界面控件是如何初始化的呢? 我们需要查看ui成员变量的类型Ui::MainWindow的实现,注意Ui::Mainwindow类与MainWindow类是不同的两个类,Ui::MainWindow类是在命名空间Ui下的类,而MainWindow是没有命名空间的,我们在mainwindow.h中可以看到:

1

MainWindow中的私有成员变量ui实际上是Ui::MainWindow类型的指针,那么Ui::MainWindow是如何定义的呢? 用鼠标点进去就看到了:

1

从这里就可以看出为什么我们的MainWindow类的构造函数中一进来就调用ui->setupUi(this)去初始化界面了

QT中的布局

QT中有四种布局方式,分别是:Vertical垂直布局、Horizontal水平布局、Grid布局、Form布局,效果如下:

1

其实Grid布局感觉跟HTML中的Table差不多,Form布局好像也是表格的效果,至于这两种布局的差异在哪里我也不是很清楚,项目中基本没有用过这两种布局方式,一般而言所有的效果都可以通过水平布局和垂直布局嵌套实现。结合水平布局和垂直布局,以及他们之间的相互嵌套,再结合使用自动伸缩调节的占位控件HorizontalSpacer和VerticalSpacer就可以实现非常复杂的布局效果。

一般使用布局有两种方式,第一种即拖放这些布局控件到UI界面上,然后将希望布局的子控件拖放到这些布局控件中,但是这种方式个人认为不够灵活,特别是在控件之间希望嵌套的时候,工具箱中的布局控件如下:

1

另外一种使用方式,QT的容器控件(那些能够放子控件的控件)都可以为其指定一种布局方式,当为一个容器控件指定布局方式之后,该容器控件就会以这种布局方式来约束其所有子控件,直接在Qt设计器的容器控件中右键就可以设置:

1

我们在一个QFrame控件中放入两个子控件,一个文本框一个按钮,之后在QFrame的空白处右键单击,在其右键菜单“布局”的子菜单中就可以指定该控件的布局模式了。实际上在代码上的原理是一样的,我们在QtCreator生成的ui_mainwindow.h中可以看到关于frame以及子控件和其布局设置的代码:

1

可以看到是这么样的关系,QFrame的子控件QPushButton以及QLineEdit(文本框)在构造的时候指定的父对象就是frame,而布局对象QHBoxLayout指定的父控件对象也是frame,也就是说除了我们在界面上看到的按钮,文本框是frame的子控件以外,我们通过右键生成的布局对象(QtCreator自动生成的,其对象id也是自动生成的),也是frame的子控件,QHBoxLayout通过addWidget函数将frame的所有直接子控件添加到布局中进行布局。而我们在工具箱中拖动布局控件到顶级窗口UI界面之后,实际上QtCreator自动生成了一个QWidget作为该布局控件的容器,并且自动生成的这个QWidget的父控件就是顶级的MainWindow窗口。也就是说我们每往UI界面上拖放一个布局控件,那么QtCreator会为该布局控件自动生成一个QWidget作为该布局控件的容器(也就是父控件),并且该自动生成的QWidget的父控件就是布局控件被拖动到的位置所在的直接容器。例如:

1

当选定一个布局控件(如果该布局控件是从工具箱拖放到UI上的,则其在UI设计器上是可以看到的),或者是选择一个容器控件的时候(如果该容器控件已经通过右键的方式指定了布局方式)。这两种情况下在QtCreator的属性栏上就可以看到布局的相关属性:

1

如果是从工具箱中拖放的布局控件,那么其属性中的Margin默认都是0 ,如果是通过右键为容器控件指定的布局,那么该布局的Margin默认是9,所以这种方式下可以看到如果此时相容器控件中添加子控件,那么子控件与容器控件之间是有间隙的,除非将这里的属性手工改为0,layoutSpacing参数对于这两种方式产生的布局默认值都是6,表示该布局中的子控件之间的间隔是6

QT中的通用控件

QT中最常用的控件QPushButton(按钮)、QLineEdit(文本框)、QRadioButton(单选框)、QCheckBox(复选框)、QFrame(一般用作容器控件,配合布局)、QProgressBar(进度条控件)这些控件的使用方法都非常简单,查一下帮助文档就可以搞定,下面的章节中,我们会讲解另外的一些控件的常用但是却不是很容易找到的功能。

QVariant 类型

再讲解其他控件之前,我们需要先了解Qt中的QVariant类型,为什么呢,因为需要为控件绑定数据,就离不开对QVariant类型的了解,下面章节中我们要说到的一些控件,在绑定数据的时候就会使用QVariant类型。他除了可以包裹Qt中常见的QString,int等类型之外,还可以包裹自定义的类对象。该类型提供了一系列的构造函数以及转换函数来携带常见类型的数据,和转换到常见类型数据的方法:

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
QVariant(int val)
QVariant(uint val)
QVariant(qlonglong val)
QVariant(qulonglong val)
QVariant(bool val)
QVariant(double val)
QVariant(float val)
QVariant(const char * val)
QVariant(const QString & val)
QVariant(const QDate & val)
QVariant(const QTime & val)
QVariant(const QDateTime & val)
 
bool    toBool() const
QByteArray  toByteArray() const
QChar   toChar() const
QDate   toDate() const
QDateTime   toDateTime() const
double  toDouble(bool * ok = 0) const
float   toFloat(bool * ok = 0) const
int toInt(bool * ok = 0) const
QJsonArray  toJsonArray() const
qlonglong   toLongLong(bool * ok = 0) const
QString toString() const
QTime   toTime() const
uint    toUInt(bool * ok = 0) const
qulonglong  toULongLong(bool * ok = 0) const

这只是其中的一部分,其实还包括了一些画图相关的类型的封装,例如QPoint,QRect等,当然Qt提供的是使用频率很高的常见的类型,有时候我们需要绑定自己定义的类对象,例如实体类:

1
2
3
4
5
6
//设置
MyClass myclass;
QVariant courseModelVariant=QVariant::fromValue(myclass);
     
//获取
myclass = courseModelVariant.value<MyClass>();

这样我们就可以使用QVariant携带任意数据类型了

QComboBox控件

下拉列表框控件最常见的功能需求就是为该控件添加下拉项目,并且为每个下拉项目添加对应的自定义隐藏数据,例如在下拉列表中每一项上面显示的文字描述是给用户看的,然而在程序中,我们可能需要该项目对应的隐藏数据,例如ID甚至是自定义的对象。

QComboBox类使用QComboBox::addItem(const QString &atext, const QVariant &auserData)成员函数为下拉列表添加项目,第一个参数text表示显示在下拉项中的文字,而第二个参数我们可以利用来为该项绑定自定义的数据,其类型为QVariant类型。我们可以通过QVariant类型方便的为该下拉项关联任意自定义的数据类型。

在获取数据的时候,通过QComboBox:: currentData(int role = Qt::UserRole)函数获取当前选中下拉项关联的QVariant类型的数据,也可以通过QComboBox:: itemData(int index, int role = Qt::UserRole)获取指定下拉项的关联数据。通过currentText()、itemText(int index)可以获取下拉项上显示的文本。

QTableWidget控件

QTableWidget是Qt中的表格显示控件,与C#中的Grid、GridView类似,主要是用来绑定数据。在UI设计界面中选中该控件之后可以在属性栏对控件的属性进行设置,最常用的属性有如下:

focusPolicy 焦点策略,如果设置为NoFocus可以去掉单击时候现实的单元格的虚线框
contextMenuPolicy 可以设置右键菜单
frameShape 设置外边框,一般设置为NoFrame去掉边框
editTriggers触发单元格的编辑状态,值NoEditTriggers表示不触发编辑状态
selectionMode选择模式,值ExtendedSelection表示多选
selectionBehavior选择行为,值SelectRows按行选择
showGrid是否显示网格线
rowCount行数
columnCount列数
horizontalHeaderVisible是否显示水平表头
verticalHeaderVIsible是否显示垂直表头
verticalScrollBarPolicy设置垂直滚动条策略
horizontalScrollBarPolicy设置水平滚动条策略

另外的一些比较实用的功能代码:
在单元格中添加控件:

1
2
3
4
QComboBox *comBox = new QComboBox();
comBox->addItem("F");
comBox->addItem("M");
ui->qtablewidget->setCellWidget(0,3,comBox);//这里不是setItem而是setCellWidget

为单元格添加checkBox:

1
2
3
4
5
6
QTableWidgetItem *item = new QTableWidgetItem();
//设置item的check状态的时候,item会自动变成QCheckBox的样子,
//不必通过setCellWidget专门插入QCheckBox控件
//通过item->checkState()可以获取该item是否勾选
item->setCheckState(Qt::Unchecked);
ui->tableWidgetCourseList->setItem(rowIndex, columnIndex, item);

单元格中显示字符串:

1
2
QTableWidgetItem *item = new QTableWidgetItem(QString("xx"));
ui->tableWidgetCourseList->setItem(rowIndex, columnIndex, item);

设置单元格关联的自定义数据:

1
2
3
4
QTableWidgetItem *item = new QTableWidgetItem(QString(""));
QVariant courseModelVariant=QVariant::fromValue(MyClass("xx"));
item->setData(USER_DEFINE_ROLE,courseModelVariant);
this->ui->tableWidgetCourseList->setItem(rowIndex, columnIndex, item);

获取单元格关联的自定义数据:

1
2
QTableWidgetItem * item = this->ui->tableWidgetCourseList->item(row,col);
Myclass model = item->data(USER_DEFINE_ROLE).value<MyClass>();

设置单元格中的文本对齐方式:

1
ui->tableWidgetCourseList->item(rowIndex, columnIndex)->setTextAlignment(Qt::AlignCenter);

通过x,y坐标获取所在的item对象:

1
2
3
4
QModelIndex index = ui->tableWidgetCourseList->indexAt(QPoint(x,y));
int row = index.row();
int col = index.column();
QTableWidgetItem * item = ui->tableWidgetCourseList->item(row,col);

设置表头的列宽:

1
ui->tableWidgetCourseList->horizontalHeader()->resizeSection(colIndex,20);//宽20

设置列宽自适应:

1
ui->tableWidgetCourseList->horizontalHeader()->setSectionResizeMode(colIndex,QHeaderView::Stretch);

初始化表头文本:

1
2
3
4
5
QStringList headerText;
headerText.append("列1");
headerText.append("列2");
headerText.append("列3");
ui->tableWidgetCourseList->setHorizontalHeaderLabels(headerText);

为表头添加复选框按钮:

在表头上添加复选框不能通过在表头单元格中添加QCheckBox的方式实现,必须进行重绘,下面的代码是我们自定义的表头类
myqheaderview.h的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//该类实现自定义的表头,主要是为了在表头中加入CheckBox控件
class MyQHeaderView : public QHeaderView
{
    Q_OBJECT
public:
    explicit MyQHeaderView(Qt::Orientation orientation, QWidget *parent = 0);
 
    void setChecked(bool checked);
 
signals:
    void headCheckBoxToggled(bool checked);
 
protected:
    void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const;
    void mousePressEvent(QMouseEvent *event);
 
private:
    QRect checkBoxRect(const QRect &sourceRect) const;
 
    bool m_isOn;
};

myqheadview.cpp的内容:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
MyQHeaderView::MyQHeaderView(Qt::Orientation orientation, QWidget *parent)
    : QHeaderView(orientation, parent)
    , m_isOn(false)
{
    // set clickable by default
    setChecked(false);
}
 
void MyQHeaderView::setChecked(bool checked)
{
    if (isEnabled() && m_isOn != checked)
    {
        m_isOn = checked;
        updateSection(0);
        emit headCheckBoxToggled(m_isOn);
    }
}
 
void MyQHeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
{
    painter->save();
    QHeaderView::paintSection(painter, rect, logicalIndex);
    painter->restore();
    if (logicalIndex == 0)
    {
        QStyleOptionButton option;
        if (isEnabled())
            option.state |= QStyle::State_Enabled;
        option.rect = checkBoxRect(rect);
        if (m_isOn)
            option.state |= QStyle::State_On;
        else
            option.state |= QStyle::State_Off;
        style()->drawControl(QStyle::CE_CheckBox, &option, painter);
    }
}
 
void MyQHeaderView::mousePressEvent(QMouseEvent *event)
{
    if (isEnabled() && logicalIndexAt(event->pos()) == 0)
    {
        m_isOn = !m_isOn;
        updateSection(0);
        emit headCheckBoxToggled(m_isOn);
    }
    else QHeaderView::mousePressEvent(event);
}
 
QRect MyQHeaderView::checkBoxRect(const QRect &sourceRect) const
{
    QStyleOptionButton checkBoxStyleOption;
    QRect checkBoxRect = style()->subElementRect(QStyle::SE_CheckBoxIndicator,
                                                 &checkBoxStyleOption);
    QPoint checkBoxPoint(sourceRect.x()+5,
                         sourceRect.y() +
                         sourceRect.height() / 2 -
                         checkBoxRect.height() / 2);
    return QRect(checkBoxPoint, checkBoxRect.size());
}

使用自定义表头:

1
2
MyQHeaderView*myHeader=new MyQHeaderView(Qt::Horizontal, ui->tableWidgetCourseList);
ui->tableWidgetCourseList->setHorizontalHeader(myHeader);

为QTableWidget添加一行数据实际上是根据行数和列数,循环QTableWidget的所有单元格,对每个单元格item设置数据来实现的。

QTabWidget控件

该控件类就是一个选项卡控件,有多个tab页,下面是一些实用的方法:

切换到tab:

1
ui->tabWidgetExportEdit->setCurrentIndex(tabIndex);

移除选项卡:

1
ui->tabWidgetExportEdit->removeTab(tabIndex);

关于选项卡控件的操作不多,重要的是怎么美化控件的显示,QSS将会作为单独的一篇文章来讲解如何美化Qt中的各种控件。

QWebview控件

该控件是用于在Qt中显示网页的控件,一般而言会将contextMenuPolicy属性设置为NoContextMenu隐藏系统为其提供的默认右键菜单

<1>. 加载网页:

1
2
3
ui->webViewCut->load(QUrl("http://www.baidu.com"));
//如果是本地网页,必须使用file:///的前缀作为网页地址
ui->webViewCut->load(QUrl("file:///c:/test.html "));

<2>. Qt代码中调用QWebview加载的网页中的js函数:

1
2
3
4
5
6
7
8
9
10
//先作如下设置
ui->webViewCut->page()->setForwardUnsupportedContent(true);
ui->webViewCut->page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
ui->webViewCut->page()->settings()->setAttribute(QWebSettings::PluginsEnabled, true);
ui->webViewCut->page()->settings()->setAttribute(QWebSettings::JavaEnabled, true);
ui->webViewCut->page()->settings()->setAttribute(QWebSettings::AutoLoadImages, true);
 
//然后在QWebview的loadFinished槽函数中调用js,该槽函数表示网页已经加载完毕
QString js = QString("alert(\'hello Qt!\')");
ui->webViewCut->page()->mainFrame()->evaluateJavaScript(js);

<3>. 在QWebview加载的html的js代码中调用Qt的函数:

默认情况下在QwebViewCut中的网页里面的js不能直接调用Qt中的相关功能,这涉及到安全性问题。要满足js中调用Qt的功能必须满足下面的条件:

在Qt中暴露一个对象给js,然后js就可以在网页中直接使用这个对象以及该对象的[特定]函数,要求是被暴露Qt对象必须继承自QObject类,并且在js中调用这个暴露的对象的成员函数的定义是有要求的,该对象的满足下面的要求的成员函数都可以直接被js调用:

1.必须是该对象的公共函数,并且在函数声明前面添加Q_INVOKABLE修饰,例如:

1
2
public :
 Q_INVOKABLE int TestQt();

2.如果该函数被声明成一个public slot 也可以不添加Q_INVOKABLE修饰:

1
2
public slots:
  void TestQt();

个人认为第一种方法更好,因为可以设置返回值,而Qt的槽函数是没有返回值的,都是返回void,只需要调用this->ui->webViewCut->page()->mainFrame()->addToJavaScriptWindowObject("QtObj", this); 就可以将一个Qt对象,也就是这里传递的this代表的对象,当然也可以直接传递其他对象指针,暴露给网页中的javascript,网页中的javascript在调用的时候可以直接使用 QtObj 去引用我们的Qt对象,以及通过QtObj去直接调用符合条件的Qt对象的成员函数。

那么this->ui->webViewCut->page()->mainFrame()->addToJavaScriptWindowObject("QtObj", this);代码在什么时候执行呢? 推荐是在QWebFrame的信号javaScriptWindowObjectCleared发出的时候执行,所以我们可以在当前UI界面类的构造函数中添加下面的代码:

1
2
connect(ui->webViewCut->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()),
    this, SLOT(populateJavaScriptWindowObject()));

然后在处理javaScriptWindowObjectCleared()信号的槽函数中实现上述暴露功能:

1
2
3
4
void MainWindow::populateJavaScriptWindowObject()
{
   ui->webViewCut->page()->mainFrame()->addToJavaScriptWindowObject("QtObj"this);
}

根据Qt文档上对该信号的描述javaScriptWindowObjectCleared()这个信号会在我们调用QwebViewCut::load()加载新的url之前就触发,我们在这个时候去处理这个信号,将我们需要暴露的Qt对象暴露给即将载入的网页

<4>. 将Qt的属性暴露出去供js调用,使用如下方法:

1
Q_PROPERTY(int Qtvalue READ testValue WRITE setTestValue)

将上面的语句加入到类的声明中,在private块下面就可以,最后不需要以分号结尾,例如:

1
2
private:
 Q_PROPERTY(int Qtvalue READ testValue WRITE setTestValue)

这一行的作用是将属性 Qtvalue 注册到Qt的元对象系统中,在js中可以通过名字Qtvalue来访问该属性,但在js中访问该属性的时候假设Qt暴露给js的对象为QtObj,那么在js中可以这样访问该属性:

1
2
QtObj.Qtvalue = 10; //设置该属性的时候会调用void setTestValue(int)
alert(QtObj.Qtvalue) //获取该属性的时候会调用 int testValue()

Q_PROPERTY(int Qtvalue READ testValue WRITE setTestValue)的结构如下:

1
2
Q_PROPERTY( 类型   属性名    READ     返回属性值的函数    WRITE     设置属性值的函数 )
            int   Qtvalue           int testValue()          void setTestValue(int)

也就是说在js中我们可以直接使用Qtvalue,当获取Qtvalue的值的时候会自动调用暴露对象的 int testValue() 函数 ,Qt规定其返回值必须与Q_PROPERTY语句中指定的类型相同,并且必须没有参数。当我们为Qtvalue设置值的时候会调用暴露对象的void setTestValue(int)函数,该函数必须有一个int类型的参数(类型也必须与前面Q_PROPERTY语句中指定的类型相同),并且不能有返回值。

经过实验int testValue()void setTestValue(int)函数的声明在private区域也可以,好像无所谓。其实这两个函数的名字是可以随意定的,对js暴露的属性名是Qtvalue,当访问Qtvalue属性的时候,会自动调用Q_PROPERTY声明中READ后面指定的函数去获取值,并且调用WRITE后面指定的函数去设置值,而不在乎这两个函数的名字。

另外这两个函数获取的值或者设置的值从哪里得来呢,我们可以在Qt对象中定义一个私有变量来保存这个值,而这个私有变量的名字是无所谓的,甚至如果需要的话,我们也不必保存这个值,直接在函数testValue里面返回一个常量值,也就是说是否应该定义一个私有变量来保存Qtvalue相关联的属性值,这个也不是必须的。

更多Qt QWidget与js的交互可以在Qt文档中搜索  The Qt WebKit Bridge关键字,其实Q_PROPERTY并不是专用于暴露属性给js的,Q_PROPERTY是Qt元对象系统的一部分。

<5>. 如果在QWebview加载的网页中有Flex应用程序,并且Qt中调用该QWebview加载的网页中的js函数中需要调用flex程序暴露给js的接口,那么还需要作如下设置:

"%appdata%\Macromedia\Flash Player\#Security\FlashPlayerTrust\"路径下新建xxx.cfg文件,将当前flex应用程序所在位置(也就是swf文件所在的目录)填写到该文件中即可,该xxx.cfg的名字是无所谓的,随便什么名字,在xxx.cfg文件中指定的目录路径中的swf文件的运行是被信任的。xxx.cfg文件中可以指定多个目录,每行一个。实际上%appdata%\Macromedia\Flash Player\#Security\FlashPlayerTrust\路径下也可以有多个文件名不同的cfg文件。xxx.cfg文件中指定的目录实际上可以直接指定为根目录,例如swf文件的路径是F:/xxx/yyy/zzz/test.swf,那么我们新建的xxx.cfg中的内容的第一行可以直接指定为F:/即可。

其实FlexBuilder在建立项目的时候,其生成的swf所在的目录都被添加到了%appdata%\Macromedia\Flash Player\#Security\FlashPlayerTrust\下面的flashbuilder.cfg中了,所以使用FlexBuilder调试项目的时候,运行的swf都是被信任的。

使用QSS

QSS是Qt中的样式表,用来定义Qt中控件的外观,实际上QSS的语法与属性大量参考了CSS,如果你有web的CSS开发经验,几乎没有任何障碍就可以掌握QSS,QSS中的选择器基本上与CSS中的相同,但是QSS只有几种常用的选择器类型。

QSS中选择器的类型:

<1>. 类型选择器,例如:QPushButton{} 设置所有类型是QPushButton或者继承自QPushButton的控件的样式。
<2>. 属性选择器,例如:QPushButton[flat="false"]{} 设置所有flat属性是false的QPushButton控件的样式。
<3>. 类选择器,例如:.QPushButton{} 设置所有QPushButton的样式,但是不会设置继承自QPushButton类型的控件的样式,QSS中的类选择器与CSS中的含义不同,QSS中的类选择器点号后面指定的类的名称,而CSS中的类选择器中的点号后面指定的是HTML标签中的class属性的名称。
<4>. ID选择器,例如:#okButton{} 设置所有对象名(object name)为okButton的控件的样式。
<5>. 后代选择器,例如:QDialog QPushButton{} 设置所有QDialog中的QPushButton子控件的样式,只要是QDialog的子控件都会应用该样式,包括直接或非直接的子控件。
<5>. 直接子选择器:例如 QDialog > QPushButton{} 设置所有是QDialog直接子控件的QPushButton的样式。
<6>. QSS支持选择器分组,支持选择器组合,例如:QPushButton#okButton{} 设置所有ID为okButton的QPushButton控件的样式。#okButton,#cancelButton{} 设置id为okButton、cancelButton的控件的样式。

那么如何在Qt中使用这些QSS设置控件的外观呢,一般在代码中通过调用控件对象的setStyleSheet(QString)成员函数进行设置,参数即是QSS字符串。例如:

1
ui->btnTest->setStyleSheet("border:1px solid red;");//设置按钮的边框

另外我们可以将所有的QSS放到文件中,例如main.qss,然后将该文件添加到Qt的资源文件中,在主UI界面中加载该main.qss文件,并调用主UI界面类的成员函数设置其下控件的样式:

1
2
3
4
5
6
QFile file(":/qss/main.qss");
file.open(QFile::ReadOnly);
QTextStream filetext(&file);
QString stylesheet = filetext.readAll();
this->setStyleSheet(stylesheet);
file.close();

要注意的是main.qss中设置的样式应该是针对当前UI界面上的控件的,也就是这里调用的this->setStyleSheet(stylesheet);中的this就是当前UI界面的类的实例。

有关QSS的细节很多,而且每个控件的美化技巧不同,同时QSS中还提供了伪类,子控件样式等功能,限于篇幅,本节只做一个大致的介绍,后面将会单独一篇文章详细讲解QSS的细节,以及如何美化Qt中的各种常见控件。

编码问题

之前在写Qt程序的时候,如果在原文件中的字符串直接写中文,例如有些地方需要弹出错误或者警告的对话框提示,那么提示内容就是中文信息,我发现有部分字符会出现乱码,并且有时候编译的时候会报错:error C2001: 常量中有换行符这一般是编码问题。我是这么解决的,在包含中文(即使是注释中有中文有时候也报错)的源文件的开头加入 #pragma execution_character_set("utf-8") 这一行指定文件的编码,同时使用UE编辑器打开该文件,另存为UTF-8的编码,在QtCreator中重新打开即可。遇到跟我同样问题的人也可以试一下这个办法。

QT的内存管理

这一小节说题目命名为QT的内存管理,题目有点过大,其实我在写Qt程序的时候,包括Qt的例子程序,中经常出现类似如下的代码:

1
2
3
4
5
void MainWidget::on_btnClick()
{
    QLabel * lblMessage = new QLabel(“hello”,this);
    lblMessage->show();
}

似乎Qt中new出来的控件类型都只负责new不用delete的,感到很奇怪,后来经过查资料发现很多人有同样的疑问,有人给出原因是因为Qt中的所有的控件类是继承自QObject类,如果在new的时候指定了父亲(在构造函数的参数中有parent这个参数),那么它的清理是在其父亲被delete的时候被delete的。Qt不建议程序员在代码中手工delete一个QObject,如果一定要这么做,需要使用QObject的deleteLater()函数,否则可能出现Qt正在一级一级的从一个父亲类开始清理下面的所有子对象的时候,程序中手工调用delete也去清理其中的子对象,那么这个时候就可能出现问题,所以建议使用deleteLater()函数,它会让所有事件都发送完成之后再清理该片内存。

QT的信号槽

在大多数Qt的编程中,我们通过Qt信号槽机制来对鼠标或键盘在界面上的操作进行响应处理,例如鼠标点击按钮的处理。Qt中的控件能够发出什么信号,在什么情况下发射信号,这在Qt的文档中有说明,每个不同的控件能够发射的信号种类和触发时机也是不同的。

如何为控件发射的信号指定对应的处理槽函数呢,我们有两种方式,第一种是在UI设计界面上操作:
1

 

在按钮控件上点击右键,选择“转到槽”菜单之后弹出如下的对话框:

1

 

可以看到按钮控件会发射很多信号,只要选择一个信号,点击OK之后就会生成对应的槽函数对按钮发出的该信号进行处理

1
2
3
void MainWindow::on_btnTest_clicked()
{
}

选择clicked()信号之后生成的处理该信号的槽函数,除了通过UI界面自动生成槽函数的方式以外,我们还可以在代码中自己手写槽函数,并通过QObject::connect()函数将特定对象的信号与另外一个对象的槽函数进行连接,当该对象的信号发射之后,会被关联的对象的槽函数处理。例如我们可以用下面的一行代码完成上面的功能:

1
connect(ui->btnTest,SIGNAL(clicked()),this,SLOT(on_btnTest_clicked()));

使用代码的好处是,很多控件的信号在上面的对话框中并没有显示出来,也就是说上面的对话框中其实只列出了该控件对象的一部分信号,另外如果我们的对象是在程序中通过代码动态构建的,那么我们也就需要在代码中为该控件的信号指定处理的槽函数了。上面的connect代码是我们直接在UI界面类的构造函数中写的(当然在任何地方都可以,并不一定要在构造函数中),由于UI界面类也是继承自QObject所以自然也继承了connect函数,通过connect函数我们可以将一个对象的信号与另一个对象的槽函数进行连接,当个该对象的信号发射的时候(信号的发射时机有可能在代码中调用对象的某个成员函数触发,也有可能在程序的UI界面上操作鼠标,键盘等触发)。

另外信号与槽在通过connect函数连接的时候,其参数类型必须完全一致,否则是没有效果的。实际上信号槽的原理,是依赖于Qt的元对象系统,Qt的一系列的构建工具为程序员做了很多自动化的工作,自动生成了一些代码,所以使得我们看起来只需要用connect函数进行关联之后,在信号发射的时候(通过emit发射信号),槽函数会被自动调用。在我们的Qt的项目的debug目录下,我们往往会看到很多以moc_为前缀的cpp文件,打开这些文件我们就可以看到该文件中的qt_meta_data_为前缀的静态数组里面描述了信号槽的关联信息,而在qt_static_metacall函数的实现中,我们可以大致看到通过一系列的case分支,对应的槽函数被调用。如果要详细研究Qt的信号槽的实现原理,可以研究QObject类的源码,以及Qt的元对象系统。

槽函数被slots修饰,当然它可以是普通的成员函数。信号被signals修饰。一个信号可以关联多个槽函数,当信号被发射的时候,这些槽函数依次被执行,但是执行的顺序是未知的,一个槽函数可以被多个信号关联。一个信号也可以关联另外一个信号,当该信号被发射的时候,与它关联的信号也被发射。通过disconnect函数可以取消信号与槽函数之间的关联关系。在槽函数中直接调用sender()就可以获得触发该槽函数的信号源对象,该函数是QObject的成员函数,返回的也是一个QObject类型的指针。

另外信号槽可以在不同的线程之间使用,但是使用的时候需要注意调用connect时候指定连接的方式,不同的线程之间Qt可以通过消息队列来实现信号与槽函数的关联,我经常在UI线程中关联另外一个工作线程的信号到UI界面类中的成员函数,以便在工作线程中通过发送信号的方式来调用UI主线程中的UI界面类的成员函数,来达到更新UI界面的效果。Qt中不能在工作线程中直接对UI界面控件进行操作。有关信号的连接方式可以参考这篇文章:对信号与事件的认识(http://blog.chinaunix.net/uid-25147458-id-3706122.html)

QT中绘图

 

我们可以在Qt中绘图,在Qt的控件上绘图,一般是需要重写该控件的重绘事件的,例如:

1
2
3
4
5
6
7
8
9
10
void MovieImageWidget::paintEvent(QPaintEvent*p)
{
    QPainter painter(this);
    if(this->currentImagePath!="")
    {
        QImage image(this->currentImagePath);
        QRect rect(0,0,this->width(),this->height());
        painter.drawImage(rect,image);
    }
}

在重绘事件中,我们先建立一个基于控件的QPainter对象,然后在重绘事件函数中,我们就可以利用该painter对象的一系列的绘制函数进行绘图操作了,绘制的图形会在该Painter关联的控件上显示,其原点坐标是从该控件的左上角开始的。在需要的时候我们可以手工调用控件的update()函数,这样会直接触发重绘事件进行重绘。

1

QPainter类提供的一系列的draw函数可以帮助我们绘制各种各样的图形,这里就不再举例说明,可以自行查阅Qt的帮助文档。

QT的线程

Qt的线程使用起来非常简单,我们首先要建立一个自定义的类(例如MyThread),继承自QThread,并实现其run方法即可。在使用线程的时候直接得到MyThread的实例,调用其start()函数即可启动线程,线程启动之后会自动调用其实现的run方法,该方法就是线程的执行函数,我们的线程任务就写在这里,当run退出之后线程基本就结束了,QThread有一个started和finished信号,我们可以为这两个信号指定槽函数,在线程启动和结束的时候执行一段代码进行资源的初始化和资源的释放操作。

QT中使用第三方的dll

通过QtCreator的向导可以非常方便的在Qt程序中使用第三方的dll,具体步骤如下:

1

在项目上点击右键,选择“添加库”菜单

1

选择外部库

1

指定对应的lib文件,以及头文件的包含路径,设置平台为windows,选择库的连接类型然后点击下一步

1

最后点击完成既可,可以看到实际上在Qt的个工程文件中,也就是pro文件中添加了如下的代码:

1
2
3
4
5
win32: LIBS += -L$$PWD/E://trans/ -lTransAPI
INCLUDEPATH += $$PWD/E:/trans/include
DEPENDPATH += $$PWD/E:/trans/include
win32:!win32-g++: PRE_TARGETDEPS += $$PWD/E:/trans/TransAPI.lib
else:win32-g++: PRE_TARGETDEPS += $$PWD/E:/trans/libTransAPI.a

在需要使用的地方,包含头文件之后就可以就可以直接调用库里面的函数了,使用方式与VC中没有区别。

QT中为控件添加右键菜单的方法

在Qt中QWidget控件以及其子类都可以添加右键菜单,Qt中所有界面上显示的控件基本都继承自QWidget控件,所以基本上Qt中的控件都可以添加右键菜单,下面举例说明为按钮添加右键菜单的方法:
<1>. 在UI设计界面中选中按钮,在属性栏中设置其属性contextMenuPolicy的值为CustomContextMenu(如果控件是在代码中生成,可以通过控件对象的成员函数setContextMenuPolicy()在代码中设置)
<2>. 在UI设计界面的按钮上单击右键,转到槽,在弹出的对话框中选择customContextMenuRequested(const QPoint&),单击确定,为按钮的该信号指定槽函数,在代码中可以通过connect手工关联。
<3>. 在该槽函数中生成菜单代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MainWindow::on_menu_click(bool checked)
{
    //通过sender()得到信号的发送对象,也就是哪个菜单项被单击
}
 
void MainWindow::on_btnTest_customContextMenuRequested(const QPoint &pos)
{
    QMenu *cmenu = new QMenu(ui->btnTest);
    QAction *action1 = cmenu->addAction("Menu 1");
    QAction *action2 = cmenu->addAction("Menu 2");
    QAction *action3 = cmenu->addAction("Menu 3");
    connect(action1, SIGNAL(triggered(bool)), this, SLOT(on_menu_click(bool)));
    connect(action2, SIGNAL(triggered(bool)), this, SLOT(on_menu_click(bool)));
    connect(action3, SIGNAL(triggered(bool)), this, SLOT(on_menu_click(bool)));
    cmenu->exec(QCursor::pos());
}

当然这里仅仅是demo代码,每次点击右键的时候,我们都要重新new出菜单来,这样肯定会耗费资源,这些菜单创建的代码可以放在一个全局的函数中,只需要创建一次,但是cmenu->exec(QCursor::pos());这条语句是显示菜单用的,执行之后菜单才能显示出来,所以每次槽函数被执行的时候都需要调用一次来呼出菜单。

最终显示效果如下:
1

除了上面的方法之外,还可以通过重写contextMenuEvent()事件来实现右键菜单,这里就不细说了,可以自行百度。

结束语

本篇总结性的讲解了Qt的诸多方面的知识点,有些地方限于篇幅,可能需要单独另起一篇文章进行讲解,有的是我自己也并没有完全弄透彻怕误人子弟。

由于公司需要开发一个窗口程序,要求不需要安装附带的框架,所以.NET就被排除在外了,因为公司之前有同事使用WPF开发过其他的程序,界面也比较漂亮,但是工程部的同事在外面部署的时候由于安装框架的原因经常出现各种系统问题。至于MFC太古老,学习周期长,所以也被排除,另外两个一个是Flex AIR,一个是Qt,权衡之下还是选择了Qt,经过一个月的边学边做,效果还可以。其实Qt还是比较好学的,基本上熟练掌握了QSS的话,也可以实现非常好的界面效果,而且还是跨平台的,特别是在嵌入式系统中,如果需要显示界面的话,会是一个非常好的选择。

希望这篇文章对大家有所帮助,由于篇幅比较长,虽然我已经检查过,如果发现文字错误,还希望园友不吝指正,我会及时改正。

回到顶部

posted @ 2018-03-22 11:10  通宵敲代码  阅读(961)  评论(0编辑  收藏  举报