2.4 混合方式UI设计
2.4.1 设计目的
【本节实例项目samp2_4完整代码下载地址】https://download.csdn.net/download/hongandyi/10424216
可视化UI设计无需人工编写代码去处理大量繁琐的界面组件的创建和布局管理,可以直观地进行界面设计,可以大大地提高工作效率。但是可视化UI设计也有一些缺陷,例如某些组件无法可视化的添加到界面上,比如在工具栏上无法可视化添加ComboBox组件,而用代码就可以。
采用纯代码方式进行UI设计虽然无所不能,但是设计效率太低,过程非常繁琐,而可视化UI设计简单高效。所以,能用可视化设计的就用可视化设计解决,无法解决的再用代码解决,将两种方法结合,才是高效设计UI的方法。
本节用一个实例讲解如何用混合方式创建UI,即部分界面设计用UI 设计器可视化实现,部分无法在UI设计器里实现的界面设计用代码实现。同时用这个实例讲解如何使用资源文件、如何使用Actions,如何设计主窗口里的菜单、工具栏和状态栏,这些是一般应用程序主窗口都有的功能。
图2-15是在UI设计器里设计完成的窗口。本项目的窗口类是从QMainWindow继承而来的,具有主菜单、工具栏和状态栏。这个例子实现了主菜单、主工具栏和状态栏,中间工作区里是一个TextEdit组件,用于编辑文本。
图2-15在UI设计器里完成的窗口界面
我们希望在工具栏上添加一个SpinBox组件,用于设置字体大小,还想在工具栏上添加一个FontComboBox组件,用于选择字体名称。但是在UI设计器里,当将这些组件拖放到工具栏上时,却显示不能添加到工具栏上。同样,在窗体下方的状态栏上也不能直接添加Label和ProgressBar组件。这就是UI可视化设计的局限,无法实现某些界面效果。
但是通过编写代码,可以实现期望的界面效果。图2-16就是程序运行时的窗口,可见在工具栏上添加了设置字体大小的SpinBox组件和选择字体名称的FontComboBox组件,在状态栏上添加了显示当前文件名称的标签,还添加了一个ProgressBar。通过设计相应的槽函数并与界面组件的相关信号进行关联,实现了期望的程序功能。
图2-16 程序运行时的窗口界面
这就是混合方式界面设计的效果,先将所有在UI设计器里可设计的静态UI元素都用可视化的方式实现,不能在UI设计器里可视化设计的再用代码实现。
2.4.2 创建项目并添加资源文件
创建一个Widget Application项目,在向导的创建窗口类时,选择基类QMainWindow,新建类的名称设置为QWMainWind,并选择生成窗体。
本项目主窗口有菜单和工具栏,需要使用到图标。在Qt项目中,图标可以存储在资源文件里,为此先创建一个资源文件。在Qt Creator里单击“File”→“New File or Project…”菜单项,在新建文件与项目对话框里选择“Qt Resource File”,然后按照向导的指引设置资源文件的文件名,并添加到当前项目里。
本项目创建的资源文件名为res.qrc。在项目文件目录树里,会自动创建一个与Headers, Sources, Forms并列的Resources文件组,在Resources组里有res.qrc节点。在资源文件名节点上右击,在弹出的快捷菜单中选择“Open in Editor”打开资源文件编辑器(如图2-17)。
图2-17资源文件编辑
资源文件最主要的一个功能就是存储图标和图片文件,以便在程序里使用。在资源文件里首先建一个前缀(Prefix),例如images,方法是在图2-17窗口右下方的功能区单击“Add”按钮下的“Add Prefix”,设置一个前缀名,前缀就类似于是资源的分组。然后再单击“Add”按钮下的“Add Files”选择图标文件即可。如果所选的图标文件不在本项目的子目录里,会提示复制文件到项目下的子目录。所以,最好将图标等原始文件放在项目的子目录下。
2.4.3 设计Actions
QAction是一个非常有用的类,在界面设计时创建Action,并编写其trigger()信号的槽函数。使用设计的Actions可以创建菜单项、工具栏按钮,还可以设置为QToolButton按钮的关联Action。点击这些由Action创建的菜单项、按钮就是执行Action的槽函数。
在项目文件目录树里双击qwmainwind.ui,进入UI设计器,在窗体的下方有一个Action Editor的面板,图2-18是本项目设计好之后的Actions列表。根据图标和文字就可以知道每个Action的功能。
图2-18 Action编辑器
在Action编辑器的上方有一个工具栏,可以新建、复制、粘贴、删除Action,还可以设置Action列表的显示方式。若要编辑某个Action,在列表里双击该Action即可。单击工具栏上的“New”按钮可以新建一个Action。新建或编辑Action的对话框如图2-19所示。
图2-19 新建或编辑一个Action
在此对话框里有以下的一些设置:● Text:Action的显示文字,该文字会作为菜单标题或工具栏按钮标题显示。若该标题后面有省略号,如“打开…”,则在工具栏按钮上显示时会自动忽略省略号,只显示“打开”。
● Object name:该Action的objectName。应该遵循自己的命名法则,例如以“act”开头表示这是一个Action。如图2-19中的是打开文件的Action,命名为actOpen。如果一个界面上Action比较多,还可以分组命名,如actFileOpen,actFileNew,actFileSave表示“文件”组,而actEditCut,actEditCopy,actEditPaste等表示“编辑”组。
● ToolTip:这个文字内容是当鼠标在一个菜单项或工具栏按钮上短暂停留时出现的提示文字。
● Icon:设置Action的图标,单击其右边的按钮可以从资源文件里选择图标,或者直接选择图片文件作为图标。
● Checkable:设置Action是否可以被复选,如果选中此选项,那么该Action就类似于QCheckbox可以改变其复选状态。
● Shortcut:设置快捷键,将输入光标移动到Shortcut旁边的编辑框里,然后按下想要设置的快捷键即可,如Ctrl+O。
做好这些设置后,单击“OK”按钮就可以新建或修改Action了。所有用于菜单和工具栏设计的功能都需要用Action来实现。
2.4.4 设计菜单和工具栏
建立Actions之后,就可以在主窗体上设计菜单和工具栏了。本项目的窗体类QWMainWind是从QMainWindow继承的,具有菜单栏、工具栏和状态栏。
双击项目文件目录树里的qwmainwind.ui,在UI设计器里打开此窗口。在窗口最上方显示“Type Here”的地方是菜单栏,菜单栏下方是工具栏,窗口最下方是状态栏。
在菜单栏显示“Type Here”的地方双击,出现一个编辑框,在编辑框里输入所要设计菜单的分组名称,如“文件”,然后回车,这样就创建了一个“文件”菜单分组,同样可以创建“编辑”、“格式”分组。
创建主菜单的分组后,从Action编辑器的列表里将一个Action拖放到菜单某个分组下,就可以创建一个菜单项,如同在界面上放置一个组件一样。如果需要在菜单里增加一个分隔条,双击“Add Seperator”就可以创建一个分隔条,然后拖动到需要的位置即可。如果需要删除某个菜单项或分隔条,单击右键,选择“Remove”菜单项。菜单设计的结果如图2-20所示。
图2-20完成后的菜单栏各分组下的菜单项
设计工具栏的方式也类似。将一个Action拖放到窗口的工具栏上,就会新建一个工具栏按钮。同样可以在工具栏上添加分隔条,可以移除工具栏按钮。主窗口上初始的只有一个工具栏,如果需要设计多个工具栏,在主窗口上单击右键,单击“Add Tool Bar”即可新建一个工具栏。
工具栏上的按钮的显示方式有多种,只需设置工具栏的toolButtonStyle属性,这是Qt::ToolButtonStyle枚举类型,缺省的是Qt::ToolButtonIconOnly,即只显示按钮的图标。还可以设置为:
● Qt::ToolButtonTextBesideIcon 文字显示在按钮旁边
● Qt::ToolButtonTextOnly 只显示文字
● Qt::ToolButtonTextUnderIcon 文字显示在按钮下方
还在窗体上放置了一个QTextEdit组件,其objectname设置为txtEdit。如此就完成了菜单栏、工具栏的可视化设计。
可视化设计的界面的底层实现是由ui_qwmainwind.h文件实现的。打开ui_qwmainwind.h文件,能看到下面的代码(已删除部分设置布局的代码)。
class Ui_QWMainWind
{
public:
QAction *actCut;
QAction *actCopy;
QAction *actPaste;
QAction *actFontBold;
QAction *actFontItalic;
QAction *actFontUnder;
QAction *actClose;
QAction *actOpen;
QAction *actClear;
QAction *actFont;
QAction *actNew;
QAction *actToolbarLab;
QWidget *centralWidget;
QtextEdit *txtEdit;
QSpinBox *spinBox;
QFontComboBox *fontComboBox;
QMenuBar *menuBar;
QMenu *menu;
QMenu *menu_2;
QMenu *menu_3;
QtoolBar *mainToolBar;
QStatusBar *statusBar;
void setupUi(QMainWindow *QWMainWind)
{
if (QWMainWind->objectName().isEmpty())
QWMainWind->setObjectName(QStringLiteral("QWMainWind"));
QWMainWind->resize(586, 363);
QFont font;
font.setPointSize(11);
QWMainWind->setFont(font);
actCut = new QAction(QWMainWind);
actCut->setObjectName(QStringLiteral("actCut"));
QIcon icon;
icon.addFile(QStringLiteral(":/images/images/cut.bmp"), QSize(), QIcon::Normal, QIcon::Off);
actCut->setIcon(icon);
actCopy = new QAction(QWMainWind);
actCopy->setObjectName(QStringLiteral("actCopy"));
QIcon icon1;
icon1.addFile(QStringLiteral(":/images/images/120.bmp"), QSize(), QIcon::Normal, QIcon::Off);
actCopy->setIcon(icon1);
// 创建其他action,省略...
centralWidget = new QWidget(QWMainWind);
centralWidget->setObjectName(QStringLiteral("centralWidget"));
txtEdit = new QtextEdit(centralWidget);
txtEdit->setObjectName(QStringLiteral("txtEdit"));
txtEdit->setGeometry(QRect(20, 20, 231, 221));
QFont font1;
font1.setPointSize(16);
txtEdit->setFont(font1);
QWMainWind->setCentralWidget(centralWidget);
//主窗体创建菜单栏、工具栏和状态栏
menuBar = new QMenuBar(QWMainWind);
menuBar->setObjectName(QStringLiteral("menuBar"));
menuBar->setGeometry(QRect(0, 0, 586, 23));
menu = new QMenu(menuBar);
menu->setObjectName(QStringLiteral("menu"));
menu_2 = new QMenu(menuBar);
menu_2->setObjectName(QStringLiteral("menu_2"));
menu_3 = new QMenu(menuBar);
menu_3->setObjectName(QStringLiteral("menu_3"));
QWMainWind->setMenuBar(menuBar);
mainToolBar = new QtoolBar(QWMainWind);
mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
mainToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
QWMainWind->addToolBar(Qt::TopToolBarArea, mainToolBar);
statusBar = new QStatusBar(QWMainWind);
statusBar->setObjectName(QStringLiteral("statusBar"));
QWMainWind->setStatusBar(statusBar);
//主菜单添加菜单项
menuBar->addAction(menu->menuAction());
menuBar->addAction(menu_2->menuAction());
menuBar->addAction(menu_3->menuAction());
menu->addAction(actNew);
menu->addAction(actOpen);
menu->addSeparator();
menu->addAction(actClose);
menu_2->addAction(actCut);
menu_2->addAction(actCopy);
menu_2->addAction(actPaste);
menu_2->addSeparator();
menu_2->addAction(actClear);
menu_3->addAction(actFontBold);
menu_3->addAction(actFontItalic);
menu_3->addAction(actFontUnder);
menu_3->addSeparator();
menu_3->addAction(actToolbarLab);
//主工具栏添加按钮
mainToolBar->addAction(actNew);
mainToolBar->addAction(actOpen);
mainToolBar->addAction(actClear);
mainToolBar->addSeparator();
mainToolBar->addAction(actCut);
mainToolBar->addAction(actCopy);
mainToolBar->addAction(actPaste);
mainToolBar->addSeparator();
mainToolBar->addAction(actFontItalic);
mainToolBar->addAction(actFontBold);
mainToolBar->addAction(actFontUnder);
mainToolBar->addSeparator();
retranslateUi(QWMainWind);
QMetaObject::connectSlotsByName(QWMainWind);
} // setupUi
};
ui_qwmainwind.h中定义了类Ui_QWMainWind。在public部分定义了界面所有Actions和各种组件的指针变量,在setupUi()函数里依次创建所有的Actions和界面组件,然后将Actions添加到主菜单和工具栏上。
ui_qwmainwind.h的代码很长,上面的清单已省略了若干行。查看ui_qwmainwind.h里的代码有助于了解Action、菜单、工具栏的代码实现原理。若按照实例samp2_3的方法完全手工来设计这样一个窗口无疑是一件繁琐的工作。好在采用UI设计器设计界面时,不需要动手去编写这些代码。界面设计的代码实现交给Qt去做就好了,它比手工编写代码效率高。
2.4.5 代码创建其他界面组件
至此,在UI设计器里已经设计了这个应用程序的主要界面功能。现在想要实现如图2-16的界面,在工具栏上增加一个SpinBox用于设置字体大小,增加一个FontComboBox来选择字体。当从组件面板里拖放一个SpinBox到工具栏上时,却发现工具栏“拒收”!同样,在状态栏上放一个Label和一个ProgreeBar也是被“拒收”的。这是UI设计器的局限性,某些界面效果无法用可视化设计方式实现。
为此,需要编写一些代码来实现这些无法可视化设计的界面功能。先在QWMainWind类定义里增加一些变量和函数定义,增加的定义如下:
class QWMainWind : public QMainWindow
{
private:
QLabel *fLabCurFile;//状态栏里显示当前文件的Label
QProgressBar *progressBar1;//状态栏上的进度条
QSpinBox *spinFontSize;// 字体大小spinBox
QFontComboBox *comboFont;//字体名称comboBox
void iniUI(); //代码实现的UI初始化
};
iniUI()函数用于创建这些界面组件,并添加到工具栏或状态栏,其实现代码如下:
void QWMainWind::iniUI()
{//状态栏上添加组件
fLabCurFile=new QLabel;
fLabCurFile->setMinimumWidth(150);
fLabCurFile->setText("当前文件:");
ui->statusBar->addWidget(fLabCurFile);//添加到状态栏
progressBar1=new QProgressBar;
progressBar1->setMaximumWidth(200);
progressBar1->setMinimum(5);
progressBar1->setMaximum(50);
progressBar1->setValue(ui->txtEdit->font().pointSize());
ui->statusBar->addWidget(progressBar1); //添加到状态栏
//工具栏上添加组件
spinFontSize = new QSpinBox;//文字大小 SpinBox
spinFontSize->setMinimum(5);
spinFontSize->setMaximum(50);
spinFontSize->setValue(ui->txtEdit->font().pointSize());
spinFontSize->setMinimumWidth(50);
ui->mainToolBar->addWidget(new QLabel("字体大小 "));
ui->mainToolBar->addWidget(spinFontSize); //SpinBox添加到工具栏
ui->mainToolBar->addSeparator(); //分隔条
ui->mainToolBar->addWidget(new QLabel(" 字体 "));
comboFont = new QFontComboBox;
comboFont->setMinimumWidth(150);
ui->mainToolBar->addWidget(comboFont);//添加到工具栏
setCentralWidget(ui->txtEdit);
}
iniUI()函数实现如下的功能:
● 创建一个Qlabel类型的组件fLabCurFile并将其添加到状态栏。
● 创建一个QProgressBar类型的组件progressBar1并添加到状态栏。
● 创建QSpinBox 类型组件spinFontSize并设置其属性,然后添加到工具栏。
● 创建一个QFontComboBox类型的组件并添加到工具栏里。
实现了iniUI()函数之后,在QWMainWind的构造函数里调用iniUI()函数,现在的构造函数代码实现如下:
QWMainWind::QWMainWind(QWidget *parent) : QMainWindow(parent),
ui(new Ui::QWMainWind)
{
ui->setupUi(this);
iniUI();
}
【注意】iniUI()函数一定要在 ui->setupUi(this)之后再调用,两行语句的先后顺序不能调换。因为ui->setupUi(this)实现了可视化设计的界面的创建,iniUI()是在可视化创建的界面基础之上添加其他组件,所以必须在其后面调用。
现在编译后运行就可以出现如图2-17的界面了,界面由可视化设计和代码设计混合实现。
2.4.6 Actions的功能实现
Actions是一种不可见的界面元素,主要用于菜单项、工具栏按钮的设计。Action的主要信号是trigger(),为一个Action的trigger()信号编写槽函数之后,菜单和工具栏上由此Action创建的菜单项和工具栏按钮就都关联此槽函数。
1. 编辑功能Actions的实现
用于编辑的Actions有剪切、复制、粘贴和清除,用于对txtEdit组件进行相应的操作。而QTextEdit类就有相应的槽函数,无需再编写实现代码,只需将Action的trigger()信号与txtEdit的相应槽函数进行关联即可。在Signals & Slots编辑器里设置信号与槽的关联,设计好后的几个Actions的关联见图2-21。
图2-21 信号与槽编辑器里设置关联
2. 其他Actions的功能实现
Action的主要信号是trigger()和trigger(bool),在单击菜单项或工具栏按钮时发射。
用于设置粗体、斜体、下划线的三个Actions具有Checkable属性,选择trigger(bool)信号设计槽函数更合适,该信号会将Action的复选状态作为一个参数传递,在响应代码里可以直接使用。其他Actions可选择trigger()信号生成槽函数。在Action的“Go to slot”对话框,选择信号为Action创建槽函数。
下面是用于设置字体粗体的actFontBold槽函数代码,使用了trigger(bool)信号。
void QWMainWind::on_actFontBold_triggered(bool checked)
{
QtextCharFormat fmt;
fmt=ui->txtEdit->currentCharFormat();
if (checked)
fmt.setFontWeight(QFont::Bold);
else
fmt.setFontWeight(QFont::Normal);
ui->txtEdit->mergeCurrentCharFormat(fmt);
}
其他Actions的槽函数代码略。本实例只是为介绍混合法UI设计,以及应用程序开发的基本流程和方法,程序功能的具体实现不是本例的重点。例如,“打开文件”的代码实现涉及对话框调用、文件读取、数据流等操作,这些会在后续章节里介绍。
3. Action的enabled和checked属性的更新
为了使界面和程序功能显得更加智能一点,应该根据当前的状态自动更新相关Action的checked和enabled属性,这是软件设计中常见的功能。
在本程序中,“剪切”、“复制”、“粘贴”的enabled属性应该随文本框内文字选择的状态变化而变化,“粗体”、“斜体”、“下划线”的checked属性应该随着当前文字的字体状态而自动更新。这可以针对QTextEdit的一些信号编写槽函数来实现。
在主窗体上选择文本编辑框txtEdit,在快捷菜单里调出“Go to slot”对话框。对话框里列出了QTextEdit的所有信号,有两个可以利用的信号:
● copyAvailable(bool) 信号在是否有内容可以被复制时发射,并且传递了一个布尔参数,可以利用此信号来改变actCut,actCopy的enabled属性。
● selectionChanged()信号在选择的文字发生变化时发射,利用此信号,可以读取当前文字的格式,从而更新粗体、斜体、下划线三个字体设置Actions的checked属性。
为txtEdit组件的这两个信号生成槽函数定义和函数体框架,编写代码如下:
void QWMainWind::on_txtEdit_copyAvailable(bool b)
{ //更新cut,copy,paste的enabled属性
ui->actCut->setEnabled(b);
ui->actCopy->setEnabled(b);
ui->actPaste->setEnabled(ui->txtEdit->canPaste());
}
void QWMainWind::on_txtEdit_selectionChanged()
{//更新粗体、斜体、下划线3个actions的checked属性
QtextCharFormat fmt;
fmt=ui->txtEdit->currentCharFormat(); //获取文字的格式
ui->actFontItalic->setChecked(fmt.fontItalic()); //是否斜体
ui->actFontBold->setChecked(fmt.font().bold()); //是否粗体
ui->actFontUnder->setChecked(fmt.fontUnderline()); //是否有下划线
}
2.4.7 手工创建的组件的信号与槽
在界面上还有两个用代码创建的组件,用于设置字体大小的spinFontSize和用于设置字体的comboFont,这两个组件的信号与槽的功能实现显然不能通过前面所述的可视化的方法来实现。
对于手工创建的组件需要手工编写槽函数,并将信号与槽关联起来。
首先,需要确定利用组件的哪个信号来编写响应代码。例如,用于设置字体大小的SpinBox组件最合适的信号是valueChanged(int),用于字体设置的FontComboBox组件适合使用currentIndexChanged(QString)信号。
为此需要在QWMainWind类中定义两个槽函数,以及用于进行信号与槽关联的函数iniSignalSlots(),该函数在构造函数里调用。下面是QWMainWind类中的相关定义(省略了其他代码)。
class QWMainWind : public QMainWindow
{
private:
void iniSignalSlots(); //关联信号与槽
private slots:
// 自定义槽函数
void on_spinBoxFontSize_valueChanged(int aFontSize);//改变字体大小
void on_comboFont_currentIndexChanged(const QString &arg1);//选择字体
};
下面的代码是以上三个函数的实现。
void QWMainWind::iniSignalSlots()
{ //信号与槽的关联
connect(spinFontSize,SIGNAL(valueChanged(int)),
this,SLOT(on_spinBoxFontSize_valueChanged(int)));
connect(comboFont,SIGNAL(currentIndexChanged(const QString &)),
this,SLOT(on_comboFont_currentIndexChanged(const QString &)));
}
void QWMainWind::on_spinBoxFontSize_valueChanged(int aFontSize)
{//改变字体大小的SpinBox
QtextCharFormat fmt;
fmt.setFontPointSize(aFontSize); //设置字体大小
ui->txtEdit->mergeCurrentCharFormat(fmt);
progressBar1->setValue(aFontSize);
}
void QWMainWind::on_comboFont_currentIndexChanged(const QString &arg1)
{//FontCombobox的响应,选择字体名称
QtextCharFormat fmt;
fmt.setFontFamily(arg1); //设置字体名称
ui->txtEdit->mergeCurrentCharFormat(fmt);
}
然后在QWMainWind的构造函数里调用iniSignalSlots()函数,实现手工创建的组件的信号与槽关联。
2.4.8 为应用程序设置图标
用Qt Creator创建的项目编译后的可执行文件具有缺省的图标,如果需要为应用设置一个自己的图标,其操作很简单,只需两步:
● 将一个图标文件(必须是“.ico”后缀的图标文件)复制到项目源程序目录下。
● 在项目配置文件里用RC_ICONS设置图标文件名,添加下面一行:
RC_ICONS = AppIcon.ico
其中,“AppIcon.ico”就是复制到项目源程序目录下的图标文件名称。这样设置后,编译后生成的可执行文件,以及主窗口的图标就换成设置的图标了。
至此,这个例子的全部功能就都实现了。在这个实例里,我们介绍了Actions的创建和使用,采用可视化的方式设计了大部分界面和槽函数。又采用手工编写代码的方式添加了其他的组件到界面上,并编写槽函数,进行信号与槽函数的关联。
这种可视化与代码化混合的设计方式相比于纯代码方式,避免了创建界面时大量繁琐的创建与布局工作,可以大大提高设计效率,同时又用代码实现了一些在UI设计器里无法可视化实现的一些界面效果。
【本节实例项目samp2_4完整代码下载地址】https://download.csdn.net/download/hongandyi/10424216