目录
5.0 本章简介
这一章开始接触应用程序主窗口的相关内容。对于日常见到的应用程序而言,许多都是基于主窗口的,主窗口包含了菜单栏、工具栏、状态栏和中心区域等。
这一章会详细介绍主窗口的每一个部分,还会涉及资源管理、富文本处理、拖放操作和文档打印等相关内容。
Qt中提供了以QMain Window类为核心的主窗口框架,它包含了众多相关的类,它们的继承关系如下图所示,本章中会讲解到图中每一个类的基本应用。
5.1 主窗口框架
主窗口为建立应用程序用户界面提供了一个框架,Qt提供了QMainWindow和其他一些相关的类共同完成主窗口的管理。QMainWindow类拥有自己的布局,包含以下组件:
①菜单栏(QMenuBar):菜单栏包含了一个下拉菜单项的列表,这些菜单项由QAction动作类实现。菜单栏位于主窗口的顶部,一个主窗口只能有一个菜单栏。
②工具栏(QToolBar):工具栏一般用于显示一些常用的菜单项目,也可以插入其他窗口部件,并且是可以移动的。一个主窗口可以拥有多个工具栏。
③中心部件(Central Widget):在主窗口的中心区域可以放入一个窗口部件作为中心部件,是应用程序的主要功能实现区域。一个主窗口只能拥有一个中心部件。
④Dock部件(QDock Widget):Dock部件常被称为停靠窗口,因为可以停靠在中心部件的四周,用来放置一些部件来实现一些功能,就像个工具箱一样。一个主窗口可以拥有多个Dock部件。
⑤状态栏(QStatusBar):状态栏用于显示程序的一些状态信息,在主窗口的最底部。一个主窗口只能拥有一个状态栏。
本节知识可以在帮助索引中通过Application Main Window关键字查看,列出了所有与创建主窗口应用程序相关的类,也可以查看Main Window示例程序。
5.1.1 Qt资源系统、菜单栏和工具栏
下面先来看一个例子。新建Qt Widgets应用,项目名称mymainwindow,类名默认为MainWindow,基类默认为QMainWindow,不改动。建立好项目后,在文件列表中双击mainwindow.ui文件进入设计模式,这时在设计区域出现的便是主窗口界面。
下面来添加菜单,双击左上角的“在这里输入”,修改为“文件(&F)”,这里要使用英文半角的括号,“&F”被称为加速键,表明程序运行时可以按下Alt+F键来激活该菜单。
修改完成后按下回车键,并在弹出的下拉菜单中将第一项改为“新建文件(&N)”,并按下回车键(由于版本原因,如果这里无法直接输入中文,则可以通过复制粘贴完成),效果如下图所示:
这时可以看到,下面的Action编辑器中已经有了“新建文件”动作,如下图所示。
选中该动作并将其拖入菜单栏下面的工具栏中,如下图所示。
在设计器中创建主窗口也可以在帮助索引中通过Creating Main Windows in Qt Designer关键字查看。
运行程序,按下Alt+F键就可以打开文件菜单,按下N键就可以激活新建文件菜单。需要说明的是,必须是文件菜单在激活状态时按下N键才有效,这也就是加速键与快捷键的不同之处。因为一般的菜单都有一个对应的图标,下面就来为菜单添加图标。
1.使用资源
3.3.1小节讲解标签部件时使用了图片,不过那里的图片是放在可执行程序外部的,如果图片位置发生变化,程序将无法显示图片。这里将使用Qt的资源系统来存储图片,就可以嵌入到可执行文件之中了。步骤如下:
第一步,添加Qt资源文件。往项目中添加新文件,选择Qt分类中的Qt Resource File,文件名称改为myimages,其他选项默认即可。
第二步,添加资源。建立好资源文件后会默认进入资源管理界面,打开 myimages.qrc文件。现在先到项目文件夹mymainwindow中新建一个名为image的文件夹,并在其中放入两张图标图片,比如放入了一个1.jpg和一个2.jpg图片。(注意:Qt资源系统要求资源文件必须放在与src文件同级或子级目录下,如果放在其他地方,则添加资源时会提示将文件复制到有效的位置。)
然后回到Qt Creator中,在资源管理界面单击“添加”按钮,选择“添加前缀”,然后将属性栏中的前缀改为“/image”,再单击“添加”按钮选择“添加文件”,在弹出的对话框中进入到前面新建的images文件夹中,选中那两张图片,单击“打开”即可。这时myimages.qrc文件中就出现了添加的图片列表。最后,按下Ctrl+S快捷键保存对文件的修改(注意:这一点很重要,如果没有保存,下面使用图片时将看不到图片)。
第三步,使用图片。先使用Ctrl+Tab快捷键转到mainwindow.ui文件,回到设计模式(如果先前没有打开过mainwindow. ui文件,那么该快捷键无法切换,需要直接双击该文件进行打开)。在Action编辑器中双击“新建文件”动作,这时会弹出编辑动作对话框。将对象名称改为action_New,工具提示ToolTip改为“新建文件”,然后按下“图标”后面的按钮进入选择资源界面。第一次进入该界面时,如果没有显示可用的资源,则可以单击左上角的重新加载绿色箭头图标,这时图片资源就显示出来了。这里选择1.jpg图片并单击OK按钮。
最后在快捷键 Shortcut后面的输入栏上单击并按下Ctrl+N组合键,就可以将它设为这个动作的快捷键了。这样就为动作添加了图标和快捷键,如下图所示:
单击OK按钮关闭对话框。因为设置了工具提示,所以运行程序时可以将鼠标停在工具栏的一个动作上来显示提示信息。(把新建文件的按钮变成了图标....还带提示)
可以在帮助索引中查看The Qt Resource System关键字,这里介绍了Qt资源系统的相关内容。前面在使用资源时添加的qrc资源文件其实是一个 XML格式的文本文件,现在进入编辑模式,在 myimages.qrc文件上右击,在弹出的级联菜单中选择“Open With→普通文本编辑器”,这时就会看到myimages.qrc的内容如下:
<RCC> <qresource prefix="/image"> <file>image/1.jpg</file> <file>image/2.jpg</file> </qresource> </RCC>
这里指明了文件类型为RCC,表明是Qt资源文件。然后是资源前缀,其下面罗列了添加的图片路径。如果编写代码时要使用1.jpg图片,那么就可以将其路径指定为“:/image/images/1.png(相对路径)”,这样就表明在使用资源文件中的图片;其中添加的前缀“/image”只是用来表明这是图片资源,可以改为别的名字,也可以为空。前面在Resource Editor中进行图片添加操作,这种方式比较方便明了,也可以按照这里的格式使用手写代码来添加图片。
往项目中添加了一个资源文件时,会自动往工程文件 mymainwindow.pro中添加代码:RESOURCES += \ myiamges.qrc
这表明项目中使用了资源文件 myimages.qrc。在前面的过程中这是自动生成的,但是如果是自己添加的已有的资源文件,要想在项目中使用,那么就要手动添加这行代码。其实资源可以是任意类型的,不只是图片文件。编译时会对加入的资源自动压缩,这也就是为什么有时生成的release版本可执行文件比添加进去的资源还要小的原因。
如何将别人的资源文件变成自己的:只需要添加上述代码即可:
RESOURCES += \ myiamges.qrc
2.编写代码方式添加菜单
前面在设计器中添加了文件菜单,然后添加了新建文件子菜单,其实这些都可以使用代码来实现。下面使用代码来添加一个菜单,在mainwindow.cpp 文件的MainWindow类构造函数中添加代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); //创建菜单对象并加入新菜单 QMenu * editMenu = ui -> menubar -> addMenu(tr("编辑(&E)")); //添加工具栏:选择图片代替和文本提示、快捷键;菜单栏新建一个条目 QAction * action_Open = editMenu -> addAction(QIcon(":/image/image/2.jpg"),tr("打开文件(&O)")); action_Open -> setShortcut(QKeySequence("Ctrl + O")); //添加工具栏 ui -> toolBar -> addAction(action_Open); }
这里使用ui→menubar 来获取QMain Window的菜单栏,使用ui→toolbar来获取QMain Window的工具栏,然后分别使用相应的函数添加菜单和动作。
就像前面提到过的,在菜单中的各种菜单项都是一个QAction类对象,这个后面还会讲到现在运行程序就可以看到已经添加了新的菜单了。
3.菜单栏
QMenuBar类提供了一个水平的菜单栏,在QMain Window 中可以直接获取默认的菜单栏,向其中添加QMenu类型的菜单对象,然后向弹出的菜单中添加QAction类型的动作对象作为菜单项。
QMenu中还提供了间隔器,可以在设计器中向添加菜单那样直接添加间隔器,或者在代码中使用addSeparator()函数来添加,它是一条水平线,可以将菜单进行分组,使得布局很整齐。
应用程序中很多普通的命令都是通过菜单来实现的,而我们也希望能将这些菜单命令放到工具栏中以方便使用。QAction就是这样一种命令动作,可以同时放在菜单和工具栏中。一个QAction动作包含了图标、菜单显示文本、快捷键、状态栏显示文本、“What's This?”显示文本和工具提示文本。这些都可以在构建QAction类对象时在构造函数中指定。另外,还可以设置QAction的checkable属性,如果指定这个动作的checkable为 true,那么选中这个菜单时就会在它的前面显示“√”之类的表示选中状态的符号;如果该菜单有图标,就会用线框将图标围住,用来表示该动作被选中了。
下面再介绍一个动作组QActionGroup类。它可以包含一组动作QAction,可以设置这组动作中是否只能有一个动作处于选中状态,这对于互斥型动作很有用。在前面程序的Main Window类构造函数中继续添加如下代码:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); QMenu * editMenu = ui -> menubar -> addMenu(tr("编辑(&E)")); QAction * action_Open = editMenu -> addAction(QIcon(":/image/image/2.jpg"),tr("打开文件(&O)")); action_Open -> setShortcut(QKeySequence("Ctrl + O")); ui -> toolBar -> addAction(action_Open); //建立动作组 QActionGroup * group = new QActionGroup(this); //向动作组中添加动作 QAction * action_L = group -> addAction(tr("左对齐(&L)")); //设置动作checkable为true action_L -> setCheckable(true); QAction * action_R = group -> addAction(tr("右对齐(&R)")); action_R -> setCheckable(true); QAction * action_C = group -> addAction(tr("居中(&C)")); action_C -> setCheckable(true); //指定action_L为选中状态 action_L -> setChecked(true); //像菜单中加入间隔器 editMenu -> addSeparator(); //向菜单中添加动作 editMenu -> addAction(action_L); editMenu -> addAction(action_R); editMenu -> addAction(action_C); }
这里让“左对齐”“右对齐”和“居中”3个动作处于一个动作组中,然后设置“左对齐”动作为默认选中状态。可以运行程序查看效果。
4.工具栏
工具栏QToolBar类提供了一个包含了一组控件的、可以移动的面板。前面已经看到可以将QAction对象添加到工具栏中,默认只是显示一个动作的图标,可以在QToolBar的属性栏中进行更改。在设计器中查看QToolBar的属性栏。
其中, toolButtonStyle属性用来设置图标和相应文本的显示及其相对位置;
movable属性用来设置状态栏是否可以移动;
allowedArea用来设置允许停靠的位置;
iconsize属性用来设置图标的大小;
floatable属性用来设置是否可以悬浮。
工具栏中除了可以添加动作外,还可以添加其他的窗口部件,下面来看一个例子。在前面的程序中的mainwindow. cpp文件中添加头文件:
#include <QToolButton> #include <QSpinBox>
然后,在构造函数中继续添加如下代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); QMenu * editMenu = ui -> menubar -> addMenu(tr("编辑(&E)")); QAction * action_Open = editMenu -> addAction(QIcon(":/image/image/2.jpg"),tr("打开文件(&O)")); action_Open -> setShortcut(QKeySequence("Ctrl + O")); ui -> toolBar -> addAction(action_Open); //建立动作组 QActionGroup * group = new QActionGroup(this); //向动作组中添加动作 QAction * action_L = group -> addAction(tr("左对齐(&L)")); //设置动作checkable为true action_L -> setCheckable(true); QAction * action_R = group -> addAction(tr("右对齐(&R)")); action_R -> setCheckable(true); QAction * action_C = group -> addAction(tr("居中(&C)")); action_C -> setCheckable(true); //指定action_L为选中状态 action_L -> setChecked(true); //像菜单中加入间隔器 editMenu -> addSeparator(); //向菜单中添加动作 editMenu -> addAction(action_L); editMenu -> addAction(action_R); editMenu -> addAction(action_C); //创建QToolButton QToolButton * toolBtn = new QToolButton(this); toolBtn -> setText(tr("color")); //创建一个弹出菜单 QMenu * colorMenu = new QMenu(this); colorMenu -> addAction(tr("red")); colorMenu -> addAction(tr("green")); toolBtn -> setMenu(colorMenu); //为toolbutton安装弹出菜单 //设置弹出模式 toolBtn -> setPopupMode(QToolButton::MenuButtonPopup); //向工具栏添加QToolButton按钮 ui -> toolBar -> addWidget(toolBtn); //创建QSpinBox QSpinBox * spinbox = new QSpinBox(this); //向工具栏添加QSpinBox部件 ui -> toolBar -> addWidget(spinbox); }
这里创建了一个QToolButton类对象,并为它添加了一个弹出菜单,设置了弹出方式是在按钮旁边有一个向下的小箭头,可以按下这个箭头弹出菜单(默认的弹出方式是按下按钮一段时间才弹出菜单)。最后将它添加到了工具栏中。下面又在工具栏中添加了一个 QSpinBox部件,可以看到,往工具栏中添加部件可以使用addWidget()函数。
这里还要再说明一下QToolButton类。其实,当往工具栏中添加一个QAction类对象时就会自动创建了一个QToolButton实例,所以说工具栏上的动作就是一个QToolButton,这就是属性栏中会有toolButtonStyle的原因。
5.1.2 中心部件
主窗口的中心区域可以放置一个中心部件,它一般是一个编辑器或者浏览器。这里支持单文档部件,也支持多文档部件。
一般的,我们会在这里放置一个部件,然后使用布局管理器使其充满整个中心区域,并可以随着窗口的大小变化而变化。下面在前面的程序中添加中心部件。在设计模式中,往中心区域拖入一个Text Edit,然后单击界面,按下Ctrl+G,使其处于一个栅格布局中。现在可以运行程序查看效果。QTextEdit是一个高级的 WYSIWYG(所见即所得)浏览器和编辑器,支持富文本的处理,为用户提供了强大的文本编辑功能。
而与QTextEdit对应的是 QPlainTextEdit类,它提供了一个纯文本编辑器,这个类与QTextEdit类的很多功能都很相似,只不过无法处理富文本。还有一个QTextBrowser类,它是一个富文本浏览器,可以看作是QTextEdit的只读模式。因为这3个类的用法大同小异,所以以后的内容中只讲解QTextEdit类。
中心区域还可以使用多文档部件。Qt中的QMdiArea部件就是用来提供一个可以显示MDI(Multiple Document Interface)多文档界面的区域,从而有效地管理多个窗口。QMdiArea中的子窗口由QMdiSubWindow类提供,这个类有自己的布局,包含一个标题栏和一个中心区域,可以向它的中心区域添加部件。
下面更改前面的程序,在设计模式将前面添加的Text Edit部件删除,然后拖入一个MDI Area部件。在Action编辑器中的“新建文件”动作上右击,在弹出的级联菜单中选择“转到槽”,然后在弹出的对话框中选择triggered()触发信号,单击OK按钮后便转到mainwindow.cpp文件中的该信号的槽的定义处,更改如下:
void MainWindow::on_action_New_triggered() { //新建文本编译器部件 QTextEdit * edit = new QTextEdit(this); //使用QMdiArea类的addSubWindow()函数创建子窗口,以文本编译器为中心部件 QMdiSubWindow * child = ui -> mdiArea -> addSubWindow(edit); child -> setWindowTitle(tr("多文档编辑子窗口")); child -> show(); }
这里需要先添加#include <QTextEdit>和#include<QMdiSubWindow>头文件。在新建文件菜单动作的触发信号槽on_action_New_triggered()中创建了多文档区域的子窗口。这时运行程序,然后按下工具栏上的新建文件动作图标,每按下一次,就会生成一个子窗口,如图所示。
5.1.3 Dock部件
QDock Widget类提供了这样一个部件,可以停靠在QMain Window中,也可以悬浮起来作为桌面顶级窗口,称为Dock部件或者停靠窗口。Dock部件一般用于存放一些其他部件来实现特殊功能,就像一个工具箱。在主窗口中可以停靠在中心部件的四周,也可以悬浮起来被拖动到任意的地方,还可以被关闭或隐藏起来。
一个Dock部件包含一个标题栏和一个内容区域,可以向Dock部件中放入任何部件。
在设计模式中向中心区域拖入一个 Dock Widget部件,然后再向 Dock中随意拖入几个部件,比如这里拖入一个 Push Button和一个Font Combo Box。在 dock Widget的属性栏中更改其 windowTitle为“工具箱”,另外还可以设置它的 features属性,包含是否可以关闭,移动和悬浮等;还有allowedArea属性,用来设置可以停靠的区域。
下面在文件菜单中添加“显示 Dock菜单"项,然后在Action 编辑器中转到“显示Dock”动作的触发信号triggered()的槽函数,更改如下:
void MainWindow::on_action_Dock_triggered() { ui -> dockWidget_3 -> show(); }
当运行程序时关闭了Dock部件后,按下该菜单项,就可以重新显示Dock了。现在可以运行程序查看效果。
5.1.4 状态栏
QStatusBar类提供了一个水平条部件,用来显示状态信息。QMainWindow中默认提供了一个状态栏。状态信息可以被分为3类:
临时信息,如一般的提示信息;
正常信息,如显示页数和行号;
永久信息,如显示版本号或者日期。
可以使用showMessage()函数显示一个临时消息,它会出现在状态栏的最左边。
一般用addWidget()函数添加一个QLabel到状态栏上,用于显示正常信息,它会生成到状态栏的最左边,可能被临时消息掩盖。
如果要显示永久信息,则要使用addPermanentWidget()函数来添加一个如 QLabel一样的可以显示信息的部件,它会生成在状态栏的最右端,不会被临时消息掩盖。
状态栏的最右端还有一个QSizeGrip部件,用来调整窗口的大小,可以使用set-SizeGripEnabled()函数来禁用它。
因为目前的设计器还不支持直接向状态栏中拖放部件,所以需要使用代码来生成。向mainwindow.cpp文件中的构造函数里继续添加代码:
//显示临时构造信息 2000ms ui -> statusbar -> showMessage(tr("welcom to use edit"),2000); //创建标签,设置标签样式显示信息,然后将其永久部件的形式添加到状态栏 QLabel * permanant = new QLabel(this); permanant -> setFrameStyle(QFrame::Box|QFrame::Sunken); permanant -> setText("www.qt.com"); ui -> statusbar -> addPermanentWidget(permanant);
注意,这里需要添加#include <QLabel>头文件。此时运行程序可以发现,“欢迎使用多文档编辑器”字符串在显示一会儿后就自动消失了,而“www.qter.org”一直显示在状态栏最右端。
到这里,主窗口的几个主要组成部分就介绍完了。可以看到,一个QMainWindow类中默认提供了一个菜单栏、一个工具栏、一个中心区域和一个状态栏,而 Dock 部件是需要自己添加的。在设计模式的相关部件上右击可以删除菜单栏、工具栏和状态栏,也可以添加新的工具栏,当然这些操作也可以使用代码实现。
5.1.5 自定义菜单
前面已经看到,可以在工具栏中添加任意的部件,那么菜单中是否也可以使用其他部件呢?当然可以,Qt中的QWidgetAction类就提供了这样的功能。
为了实现自定义菜单,需要新建一个类,它继承自QWidgetAction类,并且在其中重新实现createWidget()函数。下面的例子中实现了这样一个菜单项:包含一个标签和一个行编辑器,可以在行编辑器中输入字符串,按下回车键就可以自动将字符串输入到中心部件文本编辑器中。
新建Qt Widgets应用,项目名称为myaction,类名默认为MainWindow,基类默认为QMainWindow不改动。建好项目后往项目中添加新文件,模板选择C++ Class,类名设置为MyAction,基类设置为QWidgetAction。
在 myaction.h文件中添加代码,完成后 myaction.h文件内容如下:
#ifndef MYACTION_H #define MYACTION_H #include <QWidgetAction> #include <QWidget> #include <QLineEdit> class MyAction : public QWidget { Q_OBJECT public: explicit MyAction(QWidget *parent = nullptr); protected: //声明函数,该函数是QWidgetAction类中的虚函数 QWidget * createWidget(QWidget * parent); signals: //新建信号,用于按下回车键时将行编译器中的内容发射出去 void getText(const QString &string); private slots: //新建槽,它用来与行编辑器的按下回车键信号关联 void sendText(); private: //声明行编辑器对象的指针 QLineEdit * lineEdit; }; #endif // MYACTION_H
下面再到myaction.cpp 添加代码。先添加头文件包含:
#include <QLineEdit> #include <QSplitter> #include <QLabel>
然后将MyAction类的构造函数修改如下:
MyAction::MyAction(QWidget *parent) : QWidgetAction(parent) { //创建行编辑器 lineEdit = new QLineEdit; //将行编辑器的按下回车信号与发送文本槽关联 connect(lineEdit,&QLineEdit::returnPressed,this,&MyAction::sendText); }
再添加create Widget()函数的定义:
QWidget * MyAction :: createWidget(QWidget *parent) { //这里使用inherits()函数判断父部件是否是菜单或者工具栏 //如果是,则创建该父部件的子部件,并且返回子部件 //如果不是,则直接返回0 if(parent->inherits("QMenu")||parent->inherits("QToolBar")) { QSplitter * splitter = new QSplitter(parent); QLabel * lable = new QLabel; lable -> setText(tr("插入文本")); splitter -> addWidget(lable); splitter -> addWidget(lineEdit); return splitter; } return 0; }
当使用该类的对象,并将其添加到一个部件上时,就会自动调用createWidget()函数。这里先判断这个部件是否是一个菜单或者工具栏,如果不是,直接返回0,不处理。如果是,就以该部件为父窗口创建了一个分裂器,并在其中添加一个标签和行编辑器,最后将这个分裂器返回。其中使用的inherits()函数在第7章会讲到。
下面是sendText()函数的定义:void MyAction::sendText() { //发射信号,将行编辑器的内容发射出去 emit getText(lineEdit->text()); //清空行编辑器中的内容 lineEdit -> clear(); }
每当在行编辑器中输入文本并按下回车键时,就会激发returnPressed()信号,这时就会调用sendText()槽。这里发射了自定义的 getText()信号,并将行编辑器中的内容清空。
下面双击mainwindow. ui文件,进入设计模式,向中心区域拖入一个Text Edit部件,并使用Ctrl+G使其处于一个栅格布局中。然后进入mainwindow.h文件中添加一个私有槽的声明:class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private: Ui::MainWindow *ui; private slots: //向编译器中添加文本 void setText(const QString & string); };
下面进入mainwindow.cpp 文件中对该函数进行定义:
//插入文本 void MainWindow::setText(const QString & string) { //将获取的文本添加到编译器中 ui -> textEdit -> setText(string); }
然后在mainwindow.cpp中添加头文件#include“myaction.h”,并在 MainWindow类的构造函数中添加如下代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); //添加菜单并且加入我们的action MyAction * action = new MyAction; QMenu * editMenu = ui -> menubar -> addMenu(tr("编辑(&E)")); editMenu -> addAction(action); //将action的getText()信号和setText()槽进行关联 connect(action,SIGNAL(getText(QString)),this,SLOT(setText(QString))); }
现在运行程序,在编辑菜单中单击自定义的菜单动作,然后输入字符并按下回车键,可以看到输入的字符自动添加到了文本编辑器中。另外,也可以将这个action添加到工具栏中。
这个例子中设计了自己的信号和槽,整个过程是这样的:在行编辑器中输入文本,然后按下回车键,这时行编辑就会发射returnPressed()信号,而这时就调用了我们的sendText()槽,在sendText()槽中又发射了getText()信号,信号中包含了行编辑器中的文本,接着又会调用setText()槽,在setText()槽中将getText()信号发来的文本输人到文本编辑器中。这样就完成了按下回车键将行编辑器中的文本输入到中心部件的文本编辑器中的操作。其实,如果所有部件都是在一个类中,则就可以直接关联行编辑器的returnPressed()信号到我们的槽中,然后进行操作。但是,这里是在MyAction和Main Window两个类之间进行数据传输,所以使用了自定义信号和槽。可以看到,如果能很好地掌握信号和槽的应用,那么实现几个类之间的数据调用是很简单的。
5.2 富文本处理
前面提到QTextEdit支持富文本的处理,什么是富文本呢?
富文本(Rich Text)或者叫富文本格式,简单来说就是在文档中可以使用多种格式,比如字体颜色,图片和表格等。
它是与纯文本(Plain Text)相对而言的,比如 Windows上的记事本就是纯文本编辑器,而Word就是富文本编辑器。Qt中提供了对富文本处理的支持,可以在帮助中通过Rich Text Processing 关键字查看,那里详细讲解了富文本处理的相关内容。
5.2.1 富文本文档结构
Qt对富文本的处理分为编辑操作和只读操作两种方式。
编辑操作使用基于光标的一些接口函数,更好地模拟了用户的编辑操作,更加容易理解,而且不会丢失底层的文档框架;
而对于文档结构的概览,则使用了只读的分层次的接口函数,有利于文档的检索和输出。可见,对于文档的读取和编辑要使用不同的两组接口。
文档的光标主要基于QTextCursor类,而文档的框架主要基于QTextDocument类。一个富文本文档的结构分为几种元素来表示:
分别是框架(QTextFrame)、文本块(QTextBlock),表格(QTextTable)和列表(QTextList)。每种元素的格式又使用相应的format类来表示分别是框架格式(QTextFrameFormat)、文本块格式(QTextBlockFormat)、表格格式(QTextTableFormat)和列表格式(QTextListFormat) ,这些格式一般在编辑文档时使用,所以常和QTextCursor类配合使用。
QTextEdit类就是一个富文本编辑器,所以在构建QTextEdit类的对象时就已经构建了一个 QTextDocument类对象和一QTextCursor类对象,只须调用它们进行相应的操作即可,如下图所示。
一个空的文档包含了一个根框架(Root frame),这个根框架又包含了一个空的文本块(Block)。框架将一个文档分为多个部分,在根框架里可以再添加文本块、子框架和表格等,一个文档的结构如上图所示。下面先来看一下框架的实际应用。
新建Qt Widgets应用,项目名称为myrichtext,类名默认为MainWindow,基类默认为QMain Window。建立好项目后,在设计模式向中心区域拖入一个Text Edit部件。然后到mainwindow.cpp文件中,先添加头文件#include<QTextFrame>,再在 Main Window类的构造函数中添加如下代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); //获取文档对象 QTextDocument * document = ui -> textEdit -> document(); //获取根框架 QTextFrame * rootFrame = document -> rootFrame(); //创建框架格式 QTextFrameFormat format; //边界颜色 format.setBorderBrush(Qt::red); //边界宽度 format.setBorder(3); //使用框架格式 rootFrame -> setFrameFormat(format); }
在构造函数中获取了编辑器中的文档对象,然后获取了文档的根框架,并且重新置了框架的格式。现在运行程序,则发现只能在红色的边框中进行输入。这里还可使用setHeight()和setWidth()函数来固定框架的高度和宽度。
下面继续添加代码,使用光标类对象,在根框架中再添加一个子框架:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); //获取文档对象 QTextDocument * document = ui -> textEdit -> document(); //获取根框架 QTextFrame * rootFrame = document -> rootFrame(); //创建框架格式 QTextFrameFormat format; //边界颜色 format.setBorderBrush(Qt::red); //边界宽度 format.setBorder(3); //使用框架格式 rootFrame -> setFrameFormat(format); QTextFrameFormat frameformat; //设置背景颜色 frameformat.setBackground(Qt::lightGray); //设置边距 frameformat.setMargin(10); //设置衬托 frameformat.setPadding(5); frameformat.setBorder(2); //设置边框样式 frameformat.setBorderStyle(QTextFrameFormat::BorderStyle_Dotted); //获取光标 QTextCursor cursor = ui -> textEdit -> textCursor(); //在光标处插入框架 cursor.insertFrame(frameformat); }
这里又建立了一个框架格式,然后获取了编辑器的光标对象,并使用这个框架格式插入了一个新的框架(将新框架插入到原有的光标处)。
这里为框架格式设置了边白,它分为边界内与本身内容间的白,即填衬(Padding),和边界外与其他内容间的空白,即边距(Margin)。框架边界的样式有实线、点线等。框架格式的属性如下图所示。
5.2.2 文本块
文本块QTextBlock类为文本文档QTextDocument提供了一个文本片段(QTextFragment)的容器。
一个文本块可以看作一个段落,但是不能使用回车换行,因为一回车换行就表示创建一个新的文本块。
QTextBlock 提供了只读接口,是前面提到的文档分层次的接口的一部分,如果QTextFrame看作一层,那么其中的QTextBlock就是另一层。文本块的格式由QTextBlockFormat类来处理,它主要涉及对齐方式,文本块四周边距,缩进等内容。而文本块中的文本内容的格式,比如字体大小,加粗、下划线等,则由QTextCharFormat类来设置。下面从例子中去理解这些内容。
继续在前面的项目中添加代码。在 mainwindow.h文件中添加私有槽的声明:private slots: void showTextFrame();
然后到mainwindow.cpp文件中添加头文件#include <QDebug>,并在构造数中添加如下代码:
QAction * action_textFrame = new QAction(tr("框架"),this); connect(action_textFrame,&QAction::triggered,this,&MainWindow::showTextFrame); ui -> toolBar -> addAction(action_textFrame);
这里新建了一个动作,然后将它的触发信号和 showTextFrame()槽关联,最后将它加入到了工具栏中。下面添加showTextFrame()槽的实现:
//遍历框架 void MainWindow::showTextFrame() { QTextDocument * document = ui -> textEdit -> document(); QTextFrame * frame = document -> rootFrame(); QTextFrame::iterator it; for(it = frame->begin();!(it.atEnd());++it) { QTextFrame * childFrame = it.currentFrame(); //获取当前框架指针 QTextBlock childBlock = it.currentBlock(); //获取当前文本块 if(childFrame) { qDebug() << "frame"; } else if(childBlock.isValid()) { qDebug() << "block :" << childBlock.text(); } } }
在这个函数中获取了文档的根框架,然后使用它的迭代器iterator(在第7章讲到)来遍历根框架中的所有子框架和文本块。在循环语句中先使用QTextFrame类的 begin()函数使iterator指向根框架最开始的元素,然后使用iterator的 atEnd()函数判断是否已经到达了根框架的最后一个元素。这里如果出现子框架,则输出一个框架的提示;如果出现文本块,则输出文本块提示和文本块的内容。
现在运行程序,然后在编辑器中输入一些内容,按下工具栏中的“框架”动作,查看一下输出栏中的信息如下图所示。
可以看到,这里只能输出根框架中的文本块和子框架,子框架中的文本块却无法历到。其实还可以使用其他方法来遍历文档的所有文本块。下面在mainwindow.h文件中继续添加私有槽private slots声明:private slots: void showTextFrame(); void showTextBlock();
然后到mainwindow.cpp文件中的构造函数里继续添加如下代码:
QAction * action_textBlock = new QAction(tr("文本块"),this); connect(action_textBlock,&QAction::triggered,this,&MainWindow::showTextBlock); ui -> toolBar -> addAction(action_textBlock);
下面添加showTextBlock()槽的定义:
void MainWindow::showTextBlock() { QTextDocument * document = ui -> textEdit -> document(); QTextBlock block = document -> firstBlock(); for(int i=0;i < document->blockCount();i++) { qDebug() << tr("文本块%1,文本块首行行号为:%2,长度为:%3,内容为:").arg(i).arg(block.firstLineNumber()).arg(block.length()) << block.text(); block = block.next(); } }
这里使用了QTextDocument类的firstBlock()函数来获取文档的第一个文本块,而blockCount()函数可以获取文档中所有文本块的个数,这样便可以使用循环语句来遍历所有文本块。每一个文本块都输出了编号、第一行行号、长度和内容,然后使用QTextBlock的 next()函数来获取下一个文本块。这里需要说明的是, tr()函数中使用“%1”等位置标记,然后在后面使用arg()添加变量作为参数,这样这些参数就会代替前面字符串中的“%1”显示出来。字符串中有几个“%”号,后面就应该有几个arg()与其对应。arg()是 QString类中的函数,因为tr()函数返回QString类对象,所以这里可以这样使用。
现在运行程序,然后在编辑器中添加一些内容,按下“文本块”动作查看效果。可以看到,行号是从0开始标记的,而且如果不使用回车换行,那么它即便在编辑器中显示在了第二行,其实还是在一个文本块里。文本块的长度是从1开始计算的,就是说,就算什么都不写,那么文本块的长度也是1,所以长度会比实际字符数多1。
下面再来看看怎样来编辑文本块及其内容的格式。前面讲到,编辑操作是使用基于光标的函数接口,我们来介绍几个常用的编辑操作。在mainwindow.h文件中添加私有槽 private slots声明:
void setTextFont(bool checked);
然后在 mainwindow.cpp文件的构造函数中继续添加代码:
QAction * action_font = new QAction(tr("字体"),this); //设置动作可以被选中 action_font -> setCheckable(true); connect(action_font,&QAction::toggled,this,&MainWindow::setTextFont); ui -> toolBar -> addAction(action_font);
这里创建了一个动作,并设置它可以被选中,然后关联它的切换信号到自定义的槽上。当动作的选中和取消选中状态切换时会触发切换信号toggled(bool),当处于选中状态时,参数bool值为true。下面是setTextFont()槽的定义:
void MainWindow::setTextFont(bool checked) { //如果处于选中状态 if(checked) { QTextCursor cursor = ui -> textEdit -> textCursor(); //文本块格式 QTextBlockFormat blockFormat; //水平居中 blockFormat.setAlignment(Qt::AlignHCenter); //使用文本块格式 cursor.insertBlock(blockFormat); //字符格式 QTextCharFormat charFormat; //背景色 charFormat.setBackground(Qt::lightGray); //字体颜色 charFormat.setForeground(Qt::blue); //使用宋体,12号,加粗,倾斜 charFormat.setFont(QFont(tr("宋体"),12,QFont::Bold,true)); //使用下划线 charFormat.setFontUnderline(true); //使用字符格式 cursor.setCharFormat(charFormat); //插入文本 cursor.insertText(tr("测试字体")); } else { } }
这里先获得了编辑器的光标,然后为其添加了文本块格式和字符格式。文本块格式主要设置对齐方式、缩进等格式,字符格式主要设置字体、颜色、下划线等格式。最后使用光标插人了一个测试文字。下面运行程序,按下“字体”动作,效果如图5-12所示。
5.2.3 表格、列表与图片
现在来看一下怎样在编辑器中插入表格、列表和图片。
在mainwindow.h文件中添加私有槽 private slots声明:void inserttable(); void insertlist(); void insertimage();
然后到mainwindow.cpp文件的构造函数中继续添加代码:
QAction * action_textTable = new QAction(tr("表格"),this); QAction * action_textList = new QAction(tr("列表"),this); QAction * action_textImage = new QAction(tr("图片"),this); connect(action_textTable,&QAction::triggered,this,&MainWindow::inserttable); connect(action_textList,&QAction::triggered,this,&MainWindow::insertlist); connect(action_textImage,&QAction::triggered,this,&MainWindow::insertimage); ui -> toolBar -> addAction(action_textTable); ui -> toolBar -> addAction(action_textList); ui -> toolBar -> addAction(action_textImage);
这里新建了3个动作,并将它们添加到工具栏中,下面是几个槽的定义:
void MainWindow::inserttable() { QTextCursor cursor = ui -> textEdit -> textCursor(); //表格样式 QTextTableFormat format; format.setCellSpacing(2); //表格外面白 format.setCellPadding(10); //表格内边白 cursor.insertTable(2,2,format); //插入两行两列的表格 } void MainWindow::insertlist() { QTextListFormat format; format.setStyle(QTextListFormat::ListDecimal); ui -> textEdit -> textCursor().insertList(format); } void MainWindow::insertimage() { QTextImageFormat format; format.setName("C:/Users/lhwnb/Desktop/1.jpg"); ui -> textEdit -> textCursor().insertImage(format); }
对于表格和列表,也可以使用QTextFrame::iterator来遍历它们,可以在帮助通过Rich Text Document Structure关键字查看。表格对应的是QTextTable类,该还提供了cellAt()函数用来获取指定的单元格;insertColumns()函数用来插入列;in-sertRows()函数用来插入行;mergeCells()函数用来合并单元格;splitCell()函数用来拆分单元格。对于一个单元格,其对应的类是QTextTableCell,其格式对应的类是QTextTableCellFormat类。列表对应的类是QTextList,该类提供了count()函数来获取列表中项目的个数;item()函数获取指定项目的文本块;removeltem()函数来删除一个项目。对于列表编号,这里使用了数字编号,更多的选项可以通过QTextListFmat ; ;Style关键字查看。对于图片,可以使用QTextImageFormat类的setHeight()和setWidth()函数来设置图斤的简度见度,^光g中街田了setName()函数来指因为图片没有对应的类,所以只能使用图片格式类,程序中使用了setName()函数来
定图片,这里需要向源码目录中放入一张图片,当然,也可以将图片放到任何地方或者使用资源文件,只需要将代码中的路径改一下即可。
5.2.4 查找功能
其实像字体格式设置等操作完全可以在QTextEdit类中直接进行。QTextEdit类提供了很多方便的函数,比如常用的复制、粘贴操作,撤销、恢复操作,放大、缩小操作等。关于这些,这里不再介绍,因为使用起来很简单,只须调用一个函数即可。
下面介绍文本查找功能,它使用的是QTextEdit类的find()函数。
在前面的程序中添加代码。在mainwindow.h文件中添加类的前置声明:
class QLineEdit; class QDialog;
然后,添加私有private对象指针:
QLineEdit * lineEdit; QDialog * findDialog;
再添加两个私有槽 private slots声明:
void textFind(); void findNext();
然后,到mainwindow.cpp文件中添加头文件:
#include <QLineEdit> #include <QDialog> #include <QPushButton> #include <QVBoxLayout>
再在构造函数中添加如下代码:
QAction * action_textfind = new QAction(tr("查找"),this); //将按下查找和textfind函数关联 connect(action_textfind,&QAction::triggered,this,&MainWindow::textFind); ui -> toolBar -> addAction(action_textfind); //创建对话框 findDialog = new QDialog(this); //创建行编译器 lineEdit = new QLineEdit(findDialog); //创建按钮 QPushButton * btn = new QPushButton(findDialog); btn -> setText("寻找下一个"); //将按下按钮和findnext函数关联 connect(btn,&QPushButton::clicked,this,&MainWindow::findNext); //创建垂直布局管理器 QVBoxLayout * layout = new QVBoxLayout; layout -> addWidget(lineEdit); layout -> addWidget(btn); findDialog -> setLayout(layout);
这里在工具栏中添加了“查找”动作,然后创建了查找对话框。下面添加两个槽的定义:
void MainWindow::findNext() { QString string = lineEdit -> text(); //使用查找函数查找指定字符串,查找方式为向下查找 bool isfind = ui -> textEdit -> find(string,QTextDocument::FindBackward); if(isfind) { qDebug() << tr("行号:%1列号:%2").arg(ui->textEdit->textCursor().blockNumber()).arg(ui->textEdit->textCursor().columnNumber()); } } void MainWindow::textFind() { findDialog -> show(); }
这里使用了find()函数进行查找。选项QTextDocument:: FindBackward表示向后查找;默认的是向前查找;另外还有QTextDocument::FindCaseSensitively表示不区分大小写,QTextDocument ::FindWholeWords表示匹配整个单词。其实,QTextEdit中的 find()函数只是为了方便使用而设计的,更多的查找功能可以使用QTextDocument类的find()函数,它有几种形式可以选择,其中还可以使用正则表达式。在查找到相应字符时,这里输出了所在的行号和列号,行号和列号都是从0开始编号的。运行程序查看效果。
5.2.5 语法高亮与HTML
使用Qt Creator编辑代码时可以发现,输入关键字时会显示不同的颜色,这就是所谓的语法高亮。Qt的富文本处理中提供了QSyntaxHighlighter类来实现语法高亮。为了实现这个功能,需要创建QSyntaxHighlighter类的子类,然后重新实现 high-lightBlock()函数,使用时直接将QTextDocument类对象指针作为其父部件指针,这样就可以自动调用highlightBlock()函数了。
首先往前面的项目中添加新文件,模板选择C++ Class,类名为MySyntaxHighlighter,基类手动设置为QSyntaxHighlighter。完成后将mysyntaxhighlighter.h文件内容更改如下:
#ifndef MYSYNTAXHIGHLIGHTER_H #define MYSYNTAXHIGHLIGHTER_H #include <QSyntaxHighlighter> class MySyntaxHighlighter : public QSyntaxHighlighter { Q_OBJECT public: explicit MySyntaxHighlighter(QTextDocument * parents = 0); private: void highlightBlock(const QString &text); }; #endif // MYSYNTAXHIGHLIGHTER_H
现在到mysyntaxhighlighter.cpp文件中,先更改构造函数为:
#include "mysyntaxhighlighter.h" MySyntaxHighlighter::MySyntaxHighlighter(QTextDocument * parent):QSyntaxHighlighter(parent) { }
然后添加highlightBlock()函数定义:
void MySyntaxHighlighter::highlightBlock(const QString &text) //高亮文本块 { //字符格式 QTextCharFormat myFormat; myFormat.setFontWeight(QFont::Bold); myFormat.setBackground(Qt::green); //要匹配的字符 QString pattern = "\\bchar\\b"; //创建正则表达式 QRegExp expression(pattern); //从位置0开始匹配字符串 int index = text.indexOf(expression); //如果匹配成功,那么返回值为字符串的起始位置,它大于或等于0 while(index>=0) { //要匹配的字符串的长度 int length = expression.matchedLength(); //对要匹配的字符串设置格式 setFormat(index,length,myFormat); //继续匹配 index = text.indexOf(expression,index + length); } }
这里主要是使用了正则表达式和QString类的indexOf()函数来进行字符串匹配如果匹配成功,则使用QSyntaxHighlighter类的 setFormat()函数来设置字符格式正则表达式和indexOf()函数第7章还会讲到。下面来使用这个自定义的类。
在 mainwindow.h文件中添加类的前置声明:class MySyntaxHighlighter;
然后添加私有对象指针:
到mainwindow.cpp文件中添加头文件:
然后在构造函数的最后添加一行代码:
这里创建了MySyntaxHighlighter类的对象,并且使用编辑器的文档对象指针作为其参数,这样每当编辑器中的文本改变时都会调用highlightBlock()函数来设置语法高亮。现在可以运行程序,输入“char”,查看一下效果。
关于语法高亮,可以查看Syntax Highlighter Example示例程序。在富文本处理中还提供了对 HTML子集的支持,可以在QLabel或者QTextEdit添加文本时使用HTMI标签或者CSS属性,具体内容可以在帮助中通过Supported HTML Subset关键字查看,下面举一个最简单的例子。
在 mainwindow.cpp文件中的构造函数最后添加下面一行代码:
这里往编辑器中添加了文本,并且使用了HTMI标签,运行程序查看效果。
前面讲到了在编辑器中使用语法高亮,那么读者可能会想到在编辑代码时另一个非常有用的功能,就是自动补全。Qt中提供了QCompleter类来实现自动补全,这个类在第3章介绍行编辑器时已经介绍过了,可以使用它来实现编辑器中的自动补全功能,这个可以参考示例程序Custom Completer。富文本处理的内容就讲到这里,其涉及的东西很多,要学好这些内容就要多动手去编写程序。帮助文档的Advanced RichText Processing里还提供了一个处理大文档的方法,需要的读者可以进行参考。对于这部分内容,可以查看Text Edit程序,这个例子是一个综合的富文本编辑器。
5.3 拖放操作
对于一个实用的应用程序,不仅希望能从文件菜单中打开一个文件,更希望可以通过拖动,直接将桌面上的文件拖入程序界面来打开,就像将源文件拖入Qt Creator 中打开一样。Qt 提供了强大的拖放机制,可以在帮助中通过Drag and Drop 关键字查看。
拖放操作分为拖动(Drag)和放下(Drop)两种操作,当数据拖动时会被存储为MIME(Multipurpose Internet Mail Extensions)类型。Qt中使用QMimeData类来表示 MIME类型的数据,并使用QDrag类来完成数据的转移,而整个拖放操作都是在几个鼠标事件和拖放事件中完成的。
5.3.1 使用拖放打开文件
下面来看一个很简单的例子,就是将桌面上的.txt文本文件拖入程序打开。
新建Qt Widgets应用,项目名称改为mydragdrop,类名和基类保持MainWindow和QMainWindow不变。建立完项目后,往界面上拖入一个Text Edit部件。然后在mainwindow.h文件中添加函数声明:class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private: Ui::MainWindow *ui; protected: //拖动进入事件 void dragEnterEvent(QDragMoveEvent * event); //放下事件 void dropEvent(QDropEvent * event); };
然后到main window.cpp文件中添加头文件:
#include <QDragEnterEvent> #include <QUrl> #include <QFile> #include <QTextStream> #include <QMimeData>
最后对两个事件处理函数进行定义:
//拖动进入事件 void MainWindow::dragEnterEvent(QDragMoveEvent *event) { //数据中是否包含url if(event -> mimeData() -> hasUrls()) { //如果是接受该动作 event -> acceptProposedAction(); } else { //否则忽略该事件 event -> ignore(); } } //放下事件 void MainWindow::dropEvent(QDropEvent *event) { //获取MIME数据 const QMimeData * mimedata = event->mimeData(); //如果数据中包含url if(mimedata->hasUrls()) { //获取url列表 QList <QUrl> urlList = mimedata -> urls(); //将其中第一个url表示为本地文件路径 QString fileName = urlList.at(0).toLocalFile(); //如果文件路径不为空 if(!fileName.isEmpty()) { //建立QFile对象并且以只读的方式打开该文件 QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; //建立文本流对象 QTextStream in(&file); //将文件中所有内容读入编辑器 ui -> textEdit -> setText(in.readAll()); } } }
当鼠标拖拽一个数据进入主窗口时,就会触发dragEnterEvent()事件处理函数,从而获取其中的 MIME数据;然后查看它是否包含 URL路径,因为拖入的文本文件实际上就是拖入了它的路径,这就是event→mimeData()→hasUrls()实现的功能。如果有这样的数据,就接收它,否则忽略该事件。QMimeData类中提供了几个函数来处理常见的 MIME数据,如表5-1所列。当松开鼠标左键,将数据放入主窗口时就会触发dropEvent()事件处理函数,这里获取了MIME数据中的URL列表。因为拖入的只有一个文件,所以获取了列表中的第一个条目,并使用toLocalFile()函数将它转换为本地文件路径。然后使用QFile和 QTextStream将文件中的数据读入编辑器中。这两个类的使用可以参见第15章。最后,进入mainwindow.cpp文件,在构造函数中添加一行代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); setAcceptDrops(true); }
这样主窗口就可以接收放下事件了。这时先运行程序,然后从桌面上将一个文本文件拖入程序主窗口界面(不是里面的 Text Edit部件中),可以看到文本编辑器中显示了文本文件中的内容。
常用MIME类型数据处理函数 测试函数 获取函数 设置函数 MIME类型 hasText() text() setText() text/plain hasHtml() html() setHtml() text/html hasUrls() urls() setUrls() text/url-list hasImage() imageData() setImageData() image/* hasColor() colorData() setColorData() application/x-color
5.3.2 自定义拖放操作
下面再来看一个在窗口中拖动图片的例子,实现的功能就是在窗口中有一个图片,可以随意拖动它。这里需要使用到自定义的 MIME类型。
新建Qt Widgets应用,项目名称改为imagedragdrop,类名和基类保持MainWindow 和QMainWindow不变。完成后,在 mainwindow.h文件中对几个事件处理函数进行声明:protected: void mousePressEvent(QMouseEvent *event); // 鼠标按下事件 void dragEnterEvent(QDragEnterEvent *event); // 拖动进入事件 void dragMoveEvent(QDragMoveEvent *event); // 拖动事件 void dropEvent(QDropEvent *event); // 放下事件
然后到mainwindow.cpp文件中添加头文件:
#include <QLabel> #include <QMouseEvent> #include <QDragEnterEvent> #include <QDragMoveEvent> #include <QDropEvent> #include <QPainter> #include <QMimeData> #include <QDrag>
在构造函数中添加如下代码:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setAcceptDrops(true); // 设置窗口部件可以接收拖入 QLabel *label = new QLabel(this); // 创建标签 QPixmap pix("../imagedragdrop/logo.png"); label->setPixmap(pix); // 添加图片 label->resize(pix.size()); // 设置标签大小为图片的大小 label->move(100,100); label->setAttribute(Qt::WA_DeleteOnClose); // 当窗口关闭时销毁图片 }
这里必须先设置部件使其可以接收拖放操作,窗口部件默认是不可以接收拖放操作的。然后创建了一个标签,并且为其添加了一张图片,这里将图片放到了项目源码目录下。下面是添加那几个事件处理函数的定义:
void MainWindow::mousePressEvent(QMouseEvent *event) //鼠标按下事件 { // 第一步:获取图片 // 将鼠标指针所在位置的部件强制转换为QLabel类型 QLabel *child = static_cast<QLabel*>(childAt(event->pos())); if(!child->inherits("QLabel")) return; // 如果部件不是QLabel则直接返回 QPixmap pixmap = *child->pixmap(); // 获取QLabel中的图片 // 第二步:自定义MIME类型 QByteArray itemData; // 创建字节数组 QDataStream dataStream(&itemData, QIODevice::WriteOnly); // 创建数据流 // 将图片信息,位置信息输入到字节数组中 dataStream << pixmap << QPoint(event->pos() - child->pos()); // 第三步:将数据放入QMimeData中 QMimeData *mimeData = new QMimeData; // 创建QMimeData用来存放要移动的数据 // 将字节数组放入QMimeData中,这里的MIME类型是我们自己定义的 mimeData->setData("myimage/png", itemData); // 第四步:将QMimeData数据放入QDrag中 QDrag *drag = new QDrag(this); // 创建QDrag,它用来移动数据 drag->setMimeData(mimeData); drag->setPixmap(pixmap);//在移动过程中显示图片,若不设置则默认显示一个小矩形 drag->setHotSpot(event->pos() - child->pos()); // 拖动时鼠标指针的位置不变 // 第五步:给原图片添加阴影 QPixmap tempPixmap = pixmap; // 使原图片添加阴影 QPainter painter; // 创建QPainter,用来绘制QPixmap painter.begin(&tempPixmap); // 在图片的外接矩形中添加一层透明的淡黑色形成阴影效果 painter.fillRect(pixmap.rect(), QColor(127, 127, 127, 127)); painter.end(); child->setPixmap(tempPixmap); // 在移动图片过程中,让原图片添加一层黑色阴影 // 第六步:执行拖放操作 if (drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction) == Qt::MoveAction) // 设置拖放可以是移动和复制操作,默认是复制操作 child->close(); // 如果是移动操作,那么拖放完成后关闭原标签 else { child->show(); // 如果是复制操作,那么拖放完成后显示标签 child->setPixmap(pixmap); // 显示原图片,不再使用阴影 } }
鼠标按下时会触发鼠标按下事件,进而执行其处理函数,这里进行了一系列操作,就像程序的注释所描述的那样,大体上可以分为6步。第1步:先获取鼠标指针所在处的部件的指针,将它强制转换为QLabel类型的指针,然后使用inherits()函数判断它是否是 QLabel类型,如果不是则直接返回,不再进行下面的操作。第2步:因为不仅要在拖动的数据中包含图片数据,还要包含它的位置信息,所以需要使用自定义的MIME类型。这里使用了QByteArray字节数组来存放图片数据和位置数据,这个类在第7章会讲到。然后使用QDataStream类将数据写入数组中,这个类在第15章会讲到。其中,位置信息是当前鼠标指针的坐标减去图片左上角的坐标而得到的差值。第3步:创建了QMimeData类对象指针,使用了自定义的 MIME类型“myimage/png”,将字节数组放入QMimeData中。第4步:为了移动数据,必须创建QDrag类对象,然后为其添加QMimeData数据。这里为了在移动过程中一直显示图片,需要使用setPixmap()函数为其设置图片。然后使用setHotSpot()函数指定了鼠标在图片上单击的位置,这里是相对于图片左上角的位置,如果不设定这个,那么在拖动图片过程中,指针会位于图片的左上角。第5步:在移动图片过程中我们希望原来的图片有所改变来表明它正在被操作,所以为其添加了一层阴影。这里的 QPainter类在第10章会讲到。第6步:执行拖动操作,这需要使用QDrag类的exec()函数,它不会影响主事件循环,所以这时界面不会被冻结。这个函数可以设定所支持的放下动作和默认的放下动作,比如这里设置了支持复制动作Qt : :CopyAction和移动动作Qt : : MoveAction,并设置默认的动作是复制。这就是说我们拖动图片,可以是移动它,也可以是进行复制,而默认的是复制操作,比如使用acceptProposedAction()函数时就是使用默认的操作。当图片被放下后,exec()函数就会返回操作类型,这个返回值由下面要讲到的 dropE-vent()函数中的设置决定。这里判断到底进行了什么操作,如果是移动操作,那么就删除原来的图片;如果是复制操作,就恢复原来的图片。
void MainWindow::dragEnterEvent(QDragEnterEvent *event) // 拖动进入事件 { // 如果有我们定义的MIME类型数据,则进行移动操作 if (event->mimeData()->hasFormat("myimage/png")) { event->setDropAction(Qt::MoveAction); event->accept(); } else { event->ignore(); } }
在这两个事件处理函数中,先判断拖动的数据中是否有自定义的MIME类型的数据,如果有,则执行移动动作Qt : : MoveAction。
void MainWindow::dropEvent(QDropEvent *event) // 放下事件 { if (event->mimeData()->hasFormat("myimage/png")) { QByteArray itemData = event->mimeData()->data("myimage/png"); QDataStream dataStream(&itemData, QIODevice::ReadOnly); QPixmap pixmap; QPoint offset; // 使用数据流将字节数组中的数据读入到QPixmap和QPoint变量中 dataStream >> pixmap >> offset; // 新建标签,为其添加图片,并根据图片大小设置标签的大小 QLabel *newLabel = new QLabel(this); newLabel->setPixmap(pixmap); newLabel->resize(pixmap.size()); // 让图片移动到放下的位置,不设置的话,图片会默认显示在(0,0)点即窗口左上角 newLabel->move(event->pos() - offset); newLabel->show(); newLabel->setAttribute(Qt::WA_DeleteOnClose); event->setDropAction(Qt::MoveAction); event->accept(); } else { event->ignore(); } }
在放下事件中,使用字节数组获取了拖放的数据,然后将其中的图片数据和位置数据读取到两个变量中,并使用它们来设置新建的标签。现在运行程序并拖动图片查看效果。
这个例子是对图片进行移动,如果想对图片进行复制,则只需要将dragEnterEvent() , dragMoveEvent()和 dropEvent()这3个函数中的event→setDropAction()函数中的参数改为Qt : :CopyAction即可。对于拖放操作的其他应用,比如根据移动的距离来判断是否开始一个拖放操作,还有剪贴板QClipboard类,都可以在帮助中通过 Dragand Drop关键字查看。
5.4 打印文档
Qt 5中的Qt Print Support模块提供了对打印的支持。最简单的,只需要使用一个QPrinter类和一个打印对话框QPrintDialog类就可以完成文档的打印操作。
这一节将简单介绍打印文档、打印预览和生产PDF文档等操作。更多的应用可以在帮助中通过Qt Print Support 关键字查看。
新建Qt Widgets应用,项目名称改为myprint,类名和基类保持MainWindow和 QMain Window 不变。完成后,打开myprint.pro文件,添加如下一行代码:
QT += printsupport
然后到设计模式向界面上拖入一个Text Edit。再到mainwindow.h文件中先添加类的前置声明:
class QPrinter;
然后添加几个槽的声明:
private slots: void doPrint(); void doPrintPreview(); void printPreview(QPrinter *printer); void createPdf();
下面到mainwindow.cpp 文件中添加头文件:
#include <QPrinter> #include <QPrintDialog> #include <QPrintPreviewDialog> #include <QFileDialog> #include <QFileInfo>
在构造函数中定义几个动作:
新建三个工具栏,并且将点击工具栏和槽函数关联在一起。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QAction *action_print = new QAction(tr("打印"),this); QAction *action_printPreview = new QAction(tr("打印预览"),this); QAction *action_pdf = new QAction(tr("生成pdf"),this); connect(action_print,SIGNAL(triggered()),this,SLOT(doPrint())); connect(action_printPreview,SIGNAL(triggered()),this,SLOT(doPrintPreview())); connect(action_pdf,SIGNAL(triggered()),this,SLOT(createPdf())); ui-> toolBar -> addAction(action_print); ui-> toolBar -> addAction(action_printPreview); ui-> toolBar -> addAction(action_pdf); }
然后添加那几个槽的定义:
void MainWindow::doPrint() // 打印 { QPrinter printer; // 创建打印机对象 QPrintDialog dlg(&printer, this); // 创建打印对话框 // 如果编辑器中有选中区域,则打印选中区域 if (ui->textEdit->textCursor().hasSelection()) dlg.addEnabledOption(QAbstractPrintDialog::PrintSelection); if (dlg.exec() == QDialog::Accepted) { // 如果在对话框中按下了打印按钮 ui->textEdit->print(&printer); // 则执行打印操作 } }
这里先建立了QPrinter类对象,它代表了一个打印设备。然后创建了一个打印对话框,如果编辑器中有选中区域,则打印该区域,否则打印整个页面。
void MainWindow::doPrintPreview() // 打印预览 { QPrinter printer; QPrintPreviewDialog preview(&printer, this); // 创建打印预览对话框 // 当要生成预览页面时,发射paintRequested()信号 connect(&preview, &QPrintPreviewDialog::paintRequested, this, &MainWindow::printPreview); preview.exec(); } void MainWindow::printPreview(QPrinter *printer) { ui->textEdit->print(printer); }
这里主要使用打印预览对话框来进行打印预览,要关联它的 paintRequested()信号到自定义的槽上,须在槽中调用编辑器的打印函数,并以传来的QPrinter类对象指针为参数。
void MainWindow::createPdf() // 生成PDF文件 { QString fileName = QFileDialog::getSaveFileName(this, tr("导出PDF文件"), QString(), "*.pdf"); if (!fileName.isEmpty()) { if (QFileInfo(fileName).suffix().isEmpty()) fileName.append(".pdf"); // 如果文件后缀为空,则默认使用.pdf QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); // 指定输出格式为pdf printer.setOutputFileName(fileName); ui->textEdit->print(&printer); } }
在生成PDF 文档的槽中,使用文件对话框来获取要保存文件的路径;如果文件名没有指定后缀,则为其添加“.pdf”后缀。然后为QPrinter对象指定输出格式和文件路径,这样就可以将文档打印成PDF格式了。
现在运行程序,如果读者的计算机上安装了打印机,那么可以测试实际打印效果,否则只能测试PDF打印效果。打印预览对话框效果如下图所示。
![]()
5.5 本章API总结
1.打印文档
QPrinter :打印机对象,代表了一个打印设备
API:setOutputFormat(QPrinter::PdfFormat); // 指定输出格式为pdf
QString fileName = QFileDialog::getSaveFileName(this, tr("导出PDF文件"),QString(), "*.pdf"); printer.setOutputFileName(fileName); ui->textEdit->print(&printer);
QPrintDialog :创建打印对话框,参数为打印机对象,窗体
比如 QPrintDialog dlg(&printer, this); 表示给printer打印机对话框创建窗体。
api介绍:
if (ui->textEdit->textCursor().hasSelection()) dlg.addEnabledOption(QAbstractPrintDialog::PrintSelection); if (dlg.exec() == QDialog::Accepted) { // 如果在对话框中按下了打印按钮 ui->textEdit->print(&printer); // 则执行打印操作 }
第一个if是说如果textEdit对话框中有鼠标选中区域的话,则打印选中区域。
第二个if是说打印整个区域。
QPrintPreviewDialog :创建打印预览对话框
QPrintPreviewDialog preview(&printer, this); // 当要生成预览页面时,发射paintRequested()信号 connect(&preview, &QPrintPreviewDialog::paintRequested,this, &MainWindow::printPreview);
5.6 小结
通过学习这一章,读者要掌握主窗口各个部件的使用,能够自行开发出一个简单的
基于QMain Window的程序。另外,本章还涉及了资源文件的使用以及信号、槽的设计,这些都是开发Qt程序的基础。富文本处理是一个庞大的体系,这里不可能讲到每一个细节,所以还需要结合帮助文档多加练习。最后的拖放操作和打印文档等内容,也可以等到使用时再去学习。