3

界面展示

图片描述

仓库

核心概念

应用程序中的主窗口

  • 主窗口是与用户进行长时间交互的顶层窗口
  • 程序的绝大多数功能直接由主窗口提供
  • 主窗口通常是应用程序启动后显示的第一个窗口
  • 装个程序由一个主窗口和多个对话框组成

Qt 中的主窗口

- Qt 开发平台中直接支持主窗口的概念
- QMainWindow 是 Qt 中主窗口的基类
- QMainWindow 继承于 QWidget 是一种容器类型的组件

clipboard.png

QMainWindow 中的封装

1. 菜单栏
2. 工具栏
3. 中心组件
4. 停靠组件
5. 状态栏

clipboard.png

QMainWindow 中的组件布局

clipboard.png

在 Qt 中与菜单相关的类组件

clipboard.png

/**
 *@brief 创建菜单栏
 */
bool MainWindow::initMenuBar()
{
    QMenuBar* mb = menuBar();
    bool ret = (mb != nullptr);

    ret = ret && initFileMenu(mb);
    ret = ret && initEditMenu(mb);
    ret = ret && initFormatMenu(mb);
    ret = ret && initViewMenu(mb);
    ret = ret && initHelpMenu(mb);

    return ret;
}

/**
 *@brief 创建下拉菜单组
 */
bool MainWindow::initFileMenu(QMenuBar* mb)
{
    QMenu* menu = new QMenu("文件(&F)", mb);
    bool ret = (menu != nullptr);

    if( ret )
    {
        QAction* action = nullptr;

        ret = ret && makeAction(action, menu, "新建(&N)", Qt::CTRL + Qt::Key_N);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFileNew()));
            menu->addAction(action);
        }

        ret = ret && makeAction(action, menu, "打开(&O)...", Qt::CTRL + Qt::Key_O);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFileOpen()));
            menu->addAction(action);
        }

        ret = ret && makeAction(action, menu,  "保存(&S)", Qt::CTRL + Qt::Key_S);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFileSave()));
            menu->addAction(action);
        }

        ret = ret && makeAction(action, menu,  "另存为(&A)...", 0);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFileSaveAs()));
            menu->addAction(action);
        }

        menu->addSeparator();

        ret = ret && makeAction(action, menu,  "页面设置(&U)...", Qt::CTRL + Qt::Key_U);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFilePageSetup()));
            menu->addAction(action);
        }

        ret = ret && makeAction(action, menu,  "打印(&P)...", Qt::CTRL + Qt::Key_P);
        if( ret )
        {
            connect(action, SIGNAL(triggered()), this, SLOT(onFilePrint()));
            menu->addAction(action);
        }

        menu->addSeparator();

        ret = ret && makeAction(action, menu,  "退出(&X)", 0);
        if( ret )
        {
            menu->addAction(action);
        }
    }

    if( ret )
    {
        mb->addMenu(menu);
    }
    else
    {
        delete menu;
    }

    return ret;
}

/**
 *@brief 创建菜单项
 */
bool MainWindow::makeAction(QAction*& action, QWidget* parent, QString text, int key)
{
    bool ret = true;

    action = new QAction(text, parent);

    if( action != nullptr )
    {
        action->setShortcut(QKeySequence(key));  // 添加快捷键
    }
    else
    {
        ret = false;
    }

    return ret;
}

主窗口中的工具栏

工具栏的概念和意义

  • 应用程序中集成各种功能实现快捷使用的一个区域
  • 工具栏并不是应用程序中必须存在的组件
  • 工具栏中的元素可以是各种组件窗口
  • 工具栏中的元素通常以图标按钮的方式存在

在 Qt 中与工具栏相关的组件

clipboard.png

/**
 *@brief 创建工具栏
 */
bool MainWindow::initToolBar()
{
    QToolBar* tb = addToolBar("工具栏");
    bool ret = true;

    tb->setIconSize(QSize(16, 16));
    tb->setFloatable(false);
    tb->setMovable(false);

    ret = ret && initFileToolItem(tb);

    tb->addSeparator();

    ret = ret && initEditToolItem(tb);

    tb->addSeparator();

    ret = ret && initFormatToolItem(tb);

    tb->addSeparator();

    ret = ret && initViewToolItem(tb);

    return ret;
}

/**
 *@brief 创建与文件操作相关的快捷项
 */
bool MainWindow::initFileToolItem(QToolBar* tb)
{
    QAction* action = nullptr;
    bool ret = true;

    ret = ret && makeAction(action, tb, "新建", ":/Res/pic/new.png");
    if( ret )
    {
        connect(action, SIGNAL(triggered()), this, SLOT(onFileNew()));
        tb->addAction(action);
    }

    ret = ret && makeAction(action, tb, "打开", ":/Res/pic/open.png");
    if( ret )
    {
        connect(action, SIGNAL(triggered()), this, SLOT(onFileOpen()));
        tb->addAction(action);
    }

    ret = ret && makeAction(action, tb, "保存", ":/Res/pic/save.png");
    if( ret )
    {
        connect(action, SIGNAL(triggered()), this, SLOT(onFileSave()));
        tb->addAction(action);
    }

    ret = ret && makeAction(action, tb, "另存为", ":/Res/pic/saveas.png");
    if( ret )
    {
        connect(action, SIGNAL(triggered()), this, SLOT(onFileSaveAs()));
        tb->addAction(action);
    }

    ret = ret && makeAction(action, tb, "打印", ":/Res/pic/print.png");
    if( ret )
    {
        connect(action, SIGNAL(triggered()), this, SLOT(onFilePrint()));
        tb->addAction(action);
    }

    return ret;
}

/**
 *@brief 创建具体的快捷项
 */
bool MainWindow::makeAction(QAction*& action, QWidget* parent, QString tip, QString icon)
{
   bool ret = true;

   action = new QAction("", parent);

   if( action != nullptr )
   {
       action->setToolTip(tip);
       action->setIcon(QIcon(icon));
   }
   else
   {
       ret = false;
   }

   return ret;
}

主窗口中的状态栏

状态栏的概念和意义

  • 状态栏是应用程序中输出简要信息的区域
  • 状态栏一般位于主窗口的最底部
  • 状态栏中的消息类型

    • 实时消息,如:当前程序状态
    • 永久消息,如:程序版本号,机构名称
    • 进度消息,如:进度条提示,百分比提示

在 Qt 中提供与状态栏相关的类组件

clipboard.png

Qt 状态栏的设计原则

  • 左边的区域用于输出实时消息
  • 右边的区域用于设置永久消息
  • addWidget 在左半部分添加组件
  • addPermanentWidget 在状态栏右半部分调价组件
/**
 *@brief 创建状态栏
 */
bool MainWindow::initStatusBar()
{
    QStatusBar* sb = statusBar();
    QLabel* label = new QLabel("D.T.TianSong");
    bool ret = true;

    if( label != nullptr )
    {
        sb->addPermanentWidget(new QLabel());

        statusLabel.setMinimumWidth(150);
        statusLabel.setAlignment(Qt::AlignCenter);
        statusLabel.setText("length: " + QString::number(0) + "    lines: " + QString::number(1));
        sb->addPermanentWidget(&statusLabel);

        statusCursorLabel.setMinimumWidth(150);
        statusCursorLabel.setAlignment(Qt::AlignCenter);
        statusCursorLabel.setText("Ln: " + QString::number(1) + "    Col: " + QString::number(1));
        sb->addPermanentWidget(&statusCursorLabel);

        label->setMinimumWidth(150);
        label->setAlignment(Qt::AlignCenter);
        sb->addPermanentWidget(label);
    }
    else
    {
        ret = false;
    }

    return ret;
}

Qt 中的文本编辑组件

Qt 中支持 3 中常用的文本编辑组件

  • QLineEdit 单行文本编辑组件
  • QTextEdit 多行富文本编辑组件
  • QPlainTextEdit 多行普通文本编辑组件

Qt 中常用文本编辑组件的集成层次图

clipboard.png

不同文本组件的特性比较

单行文本支持 多行文本支持 自定义格式支持 富文本支持
QLineEdit Yes No No No
QPlainTextEdit Yes Yes No No
QTextEdit Yes Yes Yes Yes

Qt 中常用文本编辑组件的内置功能

  • 右键弹出菜单
  • 快捷键功能(复制,粘贴,剪切,等)
/**
 *@brief 创建中心组件
 */
bool MainWindow::initMainEditor()
{
    bool ret = true;

    QPalette p = mainEditor.palette();
    p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
    p.setColor(QPalette::Inactive, QPalette::HighlightedText, p.color(QPalette::Active, QPalette::HighlightedText));
    mainEditor.setPalette(p);

    mainEditor.setParent(this);
    mainEditor.setAcceptDrops(false);
    setCentralWidget(&mainEditor);

    return ret;
}

Qt 中的 IO 操作

Qt 中 IO 操作的处理方式

  • Qt 通过统一的接口简化了文件与外部设备的操作方式
  • Qt 中的文件被看作一种特殊的外部设备
  • Qt 中的文件操作与外部设备的操作相同

  • IO操作的微本质:连续存储空间的数据读写

Qt 中 IO 设备的继承层次图

clipboard.png


  • QFile 是 Qt 中用于文件操作的类,对应到计算机上的一个文件
  • QFileInfo 类用于读取文件信息
  • QTemporaryFile 安全的创建一个全局唯一的临时文件,对象销毁时临时文件删除
void write(QString f)
{
    QFile file(f);

    if( file.open(QIODevice::WriteOnly | QIODevice::Text) )
    {
        file.write("D.T.Software\n");
        file.write("Delphi Tang\n");
        file.close();
    }
}

void read(QString f)
{
    QFile file(f);

    if( file.open(QIODevice::ReadOnly | QIODevice::Text) )
    {
        QByteArray ba = file.readLine();
        QString s(ba);

        qDebug() << s;

        file.close();
    }
}

void info(QString f)
{
    QFile file(f);
    QFileInfo info(file);

    qDebug() << info.exists();
    qDebug() << info.isFile();
    qDebug() << info.isReadable();
    qDebug() << info.isWritable();
    qDebug() << info.created();
    qDebug() << info.lastRead();
    qDebug() << info.lastModified();
    qDebug() << info.path();
    qDebug() << info.fileName();
    qDebug() << info.suffix();
    qDebug() << info.size();
}

文本流和数据流

  • Qt 中将文件类型分为 2 大类

    • 文本文件: 文件内容是可读的文本字符
    • 数据文件: 文件内容是直接的二进制数据

  • Qt 提供辅助类简化了文本文件/数据文件的读写

    • QTextStream - 写入的数据全部转换为可读文本
    • QDataStream - 写入的数据根据类型转换为二进制数据
void text_stream_test(QString f)
{
    QFile file(f);

    if( file.open(QIODevice::WriteOnly | QIODevice::Text) )
    {
        QTextStream out(&file);

        out << QString("D.T.Software") << endl;
        out << QString("Result: ") << endl;
        out << 5 << '*' << 6 << '=' << 5 * 6 << endl;

        file.close();
    }

    if( file.open(QIODevice::ReadOnly | QIODevice::Text) )
    {
        QTextStream in(&file);

        while( !in.atEnd() )
        {
            QString line = in.readLine();

            qDebug() << line;
        }

        file.close();
    }
}

void data_stream_test(QString f)
{
    QFile file(f);

    if( file.open(QIODevice::WriteOnly) )
    {
        QDataStream out(&file);

        out.setVersion(QDataStream::Qt_4_7);

        out << QString("D.T.Software");
        out << QString("Result: ");
        out << 3.14;

        file.close();
    }

    if( file.open(QIODevice::ReadOnly) )
    {
        QDataStream in(&file);
        QString dt = "";
        QString result = "";
        double value = 0;

        in.setVersion(QDataStream::Qt_4_7);

        in >> dt;
        in >> result;
        in >> value;

        file.close();

        qDebug() << dt;
        qDebug() << result;
        qDebug() << value;
    }
}
  • 不同 Qt 版本的数据流文件格式可能不同

    • 设置读写版本号:void setVersion(int v)
    • 获取读写版本号:int version() const
    • 当前数据流文件可能在不同版本的 Qt 程序间传递数据时,需要考虑版本问题

缓冲区与目录操作

Qt 中缓冲区的概念

  • 缓冲区的本质为一段连续的存储空间
  • QBuffer 是 Qt 中缓冲区相关的类
  • 在 Qt 中可以将缓冲区看作一种特殊的 IO 设备
  • 文件辅助类可以直接用于操作缓冲区

QBuffer 缓冲区的使用场合

  • 在线程间进行不同类型的数据传递
  • 缓存外部设备中的数据返回
  • 数据读取速度小于数据写入速度
void write_buffer(int type, QBuffer& buffer)
{
    if( buffer.open(QIODevice::WriteOnly) )
    {
        QDataStream out(&buffer);

        out << type;

        if( type == 0 )
        {
            out << QString("D.T.Software");
            out << QString("3.1415");
        }
        else if( type == 1 )
        {
            out << 3;
            out << 1415;
        }
        else if( type == 2 )
        {
            out << 3.1415;
        }

        buffer.close();
    }
}

void read_buffer(QBuffer& buffer)
{
    if( buffer.open(QIODevice::ReadOnly) )
    {
        int type = -1;
        QDataStream in(&buffer);

        in >> type;

        if( type == 0 )
        {
            QString dt = "";
            QString pi = "";

            in >> dt;
            in >> pi;

            qDebug() << dt;
            qDebug() << pi;
        }
        else if( type == 1 )
        {
            int a = 0;
            int b = 0;

            in >> a;
            in >> b;

            qDebug() << a;
            qDebug() << b;
        }
        else if( type == 2 )
        {
            double pi = 0;

            in >> pi;

            qDebug() << pi;
        }

        buffer.close();
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QByteArray array;
    QBuffer buffer(&array);

    write_buffer(2, buffer);
    read_buffer(buffer);
    
    return a.exec();
}

QDir 是 Qt 中功能强大的目录操作类

  • Qt 中的目录分隔符统一使用 '/'
  • QDir 能够对目标目录进行任意操作(创建,删除,重命名)
  • QDir 能够获取指定目录中的所有条目
  • QDir 能够获取系统中的所有根目录

QFileSystemWatcher 用于监控文件和目录的状态变化

  • 能够监控特定目录和文件的状态
  • 能够同时对多个目录和文件进行监控
  • 当目录或者文件改变时将触发信号
  • 可以通过信号与槽的机制捕捉信号并作出相应

文本编辑器中的数据存储

QAction 的信号

  • QAction 被点击之后会产生一个 triggered 信号
  • 通过信号与槽的机制能够捕捉对 QAction 对象的操作
  • 项目中可以将多个信号映射到同一个槽函数

文件打开操作

clipboard.png

文件保存操作

  • 定义成员变量 m_filePath 用于标记数据来源

clipboard.png

文件另存为操作

clipboard.png

int MainWindow::showQueryMessage(QString message)
{
    QMessageBox msg(this);

    msg.setIcon(QMessageBox::Question);
    msg.setWindowTitle("记事本");
    msg.setWindowFlag(Qt::Drawer);
    msg.setText(message);
    msg.setStandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);

    return msg.exec();
}

void MainWindow::preEditChange()
{
    if( m_isTextChanged )
    {
        QString path = (m_filePath != "") ? m_filePath : "无标题";
        int r = showQueryMessage(QString("是否将更改保存到\n") + "\"" + path + "\" ?");

        switch ( r )
        {
        case QMessageBox::Yes:
            saveCurrentData("保存", m_filePath);
            break;
        case QMessageBox::No:
            m_isTextChanged = false;
            break;
        case QMessageBox::Cancel:
            break;
        }
    }
}

void MainWindow::openFileEditor(QString path)
{
    if( path != "" )
    {
        QFile file(path);

        if( file.open(QIODevice::ReadOnly | QIODevice::Text) )
        {
            QTextStream in(&file);
            in.setCodec("GBK");

            mainEditor.setPlainText(in.readAll());

            file.close();

            m_filePath = path;

            m_isTextChanged = false;

            setWindowTitle(m_filePath + "- 记事本");
        }
        else
        {
            showErrorMessage(QString("打开文件失败!\n\n") + "\"" + path + "\"。");
       }
    }
}

void MainWindow::openFile(QString path)
{
    preEditChange();

    if( !m_isTextChanged )
    {
        openFileEditor(path);
    }
}

void MainWindow::onFileOpen()
{
    preEditChange();

    if( !m_isTextChanged )
    {
        QString path = showFileDialog(QFileDialog::AcceptOpen, "打开", ":/Res/pic/logo.png");

        openFileEditor(path);
    }
}

QString MainWindow::saveCurrentData(QString title, QString path)
{
    QString ret = path;

    if( ret == "" )
    {
        ret = showFileDialog(QFileDialog::AcceptSave, title, ":/Res/pic/logo.png");
    }

    if( ret != "" )
    {
        QFile file(ret);

        if( file.open(QIODevice::WriteOnly | QIODevice::Text) )
        {
            QTextStream out(&file);

            out << mainEditor.toPlainText();

            file.close();

            setWindowTitle(ret + " - 记事本");

            m_isTextChanged = false;
        }
        else
        {
            showErrorMessage(QString("保存文件失败!\n\n") + "\"。" + ret + "\"");
        }
    }

    return ret;
}

void MainWindow::onFileSave()
{
    QString path = saveCurrentData("保存", m_filePath);

    if( path != "" )
    {
        m_filePath = path;
    }
}

void MainWindow::onFileSaveAs()
{
    QString path = saveCurrentData("另存为");

    if( path != "" )
    {
        m_filePath = path;
    }
}

文本编辑器中的功能交互

QPlainTextEdit 相关的信号

  • 使用关键槽函数判断数据状态

    • void textChanged() ==> 辅助判断是否有数据未保存
    • void copyAvailabel(bool)
    • void cursorPositionChanged()
    • void redoAvailable(bool);
    • void undoAvailable(bool)

  • 判断是由存在未保存的数据

    • 定义槽函数 void onTextChanged()
    • 映射 textChanged() 到槽函数
    • 定义成员变量 bool m_isTextChanged = false;
    • 文本框中的字符发生变化时: m_isTextChanged = true
    • 当 m_isTextChanged 为真,则存在未保存的数据
void MainWindow::onTextChanged()
{
    if( !m_isTextChanged )
    {
        setWindowTitle("* " + windowTitle());
    }

    m_isTextChanged = true;

    statusLabel.setText("length: " + QString::number(mainEditor.toPlainText().length()) + "    lines: " + QString::number(mainEditor.document()->lineCount()));
}

文件新建操作

clipboard.png

void MainWindow::onFileNew()
{
    preEditChange();

    if( !m_isTextChanged )
    {
        mainEditor.clear();

        m_isTextChanged = false;

        setWindowTitle("新建文本文档 - 记事本");
    }
}

文本编辑器中的后缀映射

  • 通过 QMap 实现
QString MainWindow::showFileDialog(QFileDialog::AcceptMode mode, QString title, QString icon)
{
    QFileDialog fd(this);
    QStringList filters;
    QMap<QString, QString> map;
    const char* filterArray[][2] =
    {
        {"文本文档(*.txt)", ".txt"},
        {"所有文件(*.*)"  , ".*"   },
        {nullptr         , nullptr}
    };
    QString ret = "";

    for(int i=0; filterArray[i][0]!=nullptr; i++)
    {
        filters.append(filterArray[i][0]);
        map.insert(filterArray[i][0], filterArray[i][1]);
    }

    fd.setWindowTitle(title);
    fd.setWindowIcon(QIcon(icon));
    fd.setAcceptMode(QFileDialog::AcceptOpen);
    fd.setNameFilters(filters);

    if( mode == QFileDialog::AcceptOpen )
    {
        fd.setFileMode(QFileDialog::ExistingFile);
    }

    if( fd.exec() == QFileDialog::Accepted )
    {
        ret = fd.selectedFiles()[0];

        if( mode == QFileDialog::AcceptSave )
        {
            QString postfix = map[fd.selectedNameFilter()];

            if( (postfix != ".*") && !ret.endsWith(postfix) )
            {
                ret = ret + postfix;
            }
        }
    }

    return ret;
}

Qt 中的事件处理

图形界面应用程序的消息处理模型

clipboard.png

Qt 平台将系统产生的消息转换为 Qt 事件

  • Qt 事件用于描述程序内部或外部发生的动作
  • 任意的 QObject 对象都具备事件处理的能力

clipboard.png

GUI 应用程序的事件处理方式

  • Qt 事件产生后立即被分发到 QWidget 对象
  • QWidget 中的 event(QEvent*) 进行事件处理
  • event() 根据事件类型调用不同的事件处理函数
  • 在事件处理函数中发送 Qt 中预定义的信号
  • 调用信号关联的槽函数

事件(QEvent)和信号(SIGNAL)不同

  • 事件由具体对象进行处理
  • 信号由具体对象主动产生
  • 改写事件处理函数可能导致程序行为发生改变
  • 信号是否存在对应的槽函数不会改变程序行为
  • 一般而言,信号在具体的事件处理函数中产生

文本编辑器的关闭操作

  • Qt 没有提供预定义的关闭信号,因此重写关闭事件
/**
 *@brief 重写关闭事件处理函数
 */
void MainWindow::closeEvent(QCloseEvent *event)
{
    preEditChange();

    if( !m_isTextChanged )
    {
        QFont font = mainEditor.font();
        bool isWrap = (mainEditor.lineWrapMode() == QPlainTextEdit::WidgetWidth);
        bool tbVisible = (findMenuBarAction("工具栏")->isCheckable() && findToolBarAction("工具栏")->isChecked());
        bool sbVisible = (findMenuBarAction("状态栏")->isCheckable() && findToolBarAction("状态栏")->isChecked());
        AppConfig config(mainEditor.font(), size(), pos(), isWrap, tbVisible, sbVisible, this);

        config.store();

        QMainWindow::closeEvent(event);
    }
    else
    {
        event->ignore();
    }
}

/**
 *@brief 查找菜单栏中对应的 ACtion
 */
QAction* MainWindow::findMenuBarAction(QString text)
{
    QAction* ret = nullptr;
    const QObjectList& list = menuBar()->children();

    for(int i=0; i<list.count(); i++)
    {
        QMenu* men = dynamic_cast<QMenu*>(list[i]);

        if( men != nullptr )
        {
            QList<QAction*> actions = men->actions();

            for(int j=0; j<actions.count(); j++)
            {
                if( actions[j]->text().startsWith(text) )
                {
                    ret = actions[j];

                    break;
                }
            }
        }
    }

    return ret;
}

/**
 *@brief 查找工具栏中对应的 ACtion
 */
QAction* MainWindow::findToolBarAction(QString text)
{
    QAction* ret = nullptr;

    QList<QAction*> actions = toolBar()->actions();

    for(int j=0; j<actions.count(); j++)
    {
        if( actions[j]->toolTip().startsWith(text) )
        {
            ret = actions[j];
            break;
        }
    }

    return ret;
}

Qt 中的拖放事件

  • 拖放一个文件进入窗口时将触发拖放事件
  • 每一个 QWidget 对象都能够处理拖放事件
  • 拖放事件的处理函数为:

    • void dragEnterEvent(QDragEnterEvent* e);
    • void dropEvent(QDropEvent* e);

拖放事件中的 QMimeData

  • QMimeData 是 Qt 中的多媒体数据类
  • 拖放事件通过 QMimeData 对象传递数据
  • QMimeData 支持多种不同类型的多媒体数据

常用 MIME 类型数据处理函数

clipboard.png

自定义拖放事件的步骤

  • 对接收拖放事件的对象调用 setAcceptDrop 成员函数
  • 重写 dragEnterEvent 函数并判断 MIME 类型

    • 期望数据: e->acceptProposedAction();
    • 其他数据: e->ignore();
  • 重写 dropEvent 函数并判断 MIMI 类型

    • 期望数据: 从事件对象中获取 MIME 数据并处理
    • 其它数据: e->ignore();

文本编辑器中的拖放操作

clipboard.png

void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
    if( event->mimeData()->hasUrls() )
    {
        event->acceptProposedAction();
    }
    else
    {
        event->ignore();
    }
}

void MainWindow::dropEvent(QDropEvent* event)
{
    if( event->mimeData()->hasUrls() )
    {
        QList<QUrl> list = event->mimeData()->urls();
        QString path = list[0].toLocalFile();
        QFileInfo fi(path);

        if( fi.isFile() )
        {
            preEditChange();

            if( !m_isTextChanged )
            {
                openFileEditor(path);
            }
        }
        else
        {
            showErrorMessage(QString("对 ") + "\"" + path + "\" 的访问被拒绝。");
        }
    }
    else
    {
       event->ignore();
    }
}

文本打印与光标定位

QPlainTextEdit 内部的文档结构(数据与界面分离)

  • QPlainTextEdit 通过 QTextDocument 对象存储文本
  • QPlainTextEdit 本身只负责界面形态的显示

clipboard.png

  • QTextDocument 是表示文本以及文本属性的数据类

    • 设置文本属性: 排版,字体,标题,等
    • 获取文本参数: 行数,文本宽度,文本信息,等
    • 实现标准操作:撤销,重做,查找,打印,等

打印功能的实现步骤

  • 连接 QAction 打印对象的信号到槽
  • 在槽函数中定义 QPrintDialog 对象
  • 根据用户选择获取 QPrinter 对象
  • 通过 QTextDocument 对象进行打印
void MainWindow::onFilePrint()
{
    QPrintDialog dlg(this);

    dlg.setWindowTitle("打印");

    if( dlg.exec() == QPrintDialog::Accepted )
    {
        QPrinter* p = dlg.printer();

        p->setPageLayout(m_pPageSetupDlg->printer()->pageLayout());

        mainEditor.document()->print(p);
    }
}

光标位置的计算

  • 思路

    • 文本框对象的内部包含了 QTextCursor 对象
    • 通过 position() 成员函数获取当前光标的字符位置
    • 根据光标的字符位置计算横纵坐标
    • 当光标位置发生变化时进行计算
  • 算法流程描述

    • 通过 'n' 字符的个数计算所在行
    • 通过最后一个 'n' 字符的下标计算所在列

clipboard.png

void MainWindow::onCursorPositionChanged()
{
    int col = 0;
    int ln = 0;
    int flg = -1;
    int pos = mainEditor.textCursor().position();
    QString text = mainEditor.toPlainText();

    for(int i=0; i<pos; i++)
    {
        if( text[i] == '\n' )
        {
            ln ++;
            flg = i;
        }
    }

    flg ++;

    col = pos - flg;

    statusCursorLabel.setText("Ln: " + QString::number(ln + 1) + "    Col: " + QString::number(col + 1));
}

在程序中发送自主事件

  • 阻塞型事件发送: 时间发送后需要等待事件处理完成
  • 非阻塞型事件发送:事件发送后立即返回; 事件被发送到事件队列中等待处理

  • QApplication 类提供了支持事件发送的静态成员函数

    • 阻塞型发送函数: bool sendEvent(QObject receiver, QEvent event);
    • 非阻塞型事件发送函数: bool postEvent(QObject receiver, QEvent event);

  • 注意事项

    • sendEvent 中事件对象的生命期由 Qt 平台管理

      • 同时支持栈事件对象和堆事件对象
    • postEvent 中事件对象的生命期由 Qt 平台管理

      • 只能发送堆事件对象
      • 事件被处理后由 Qt 平台销毁

  • 使用 sendEvent 发送事件对象
  • 消息发送过程可以理解为:在 sendEvent() 函数内部直接调用 Qt 对象的event() 事件处理

clipboard.png

  • 使用 postEvent 发送事件对象

clipboard.png

菜单栏中删除功能的实现

clipboard.png

  • 定义事件对象 KeyPress
  • 定义事件对象 KeyRelease
  • 发送事件 KePress
  • 发送事件 KeyRelease
void MainWindow::onEditDelete()
{
    QKeyEvent keyPress(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
    QKeyEvent keyRelease(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);

    QApplication::sendEvent(&mainEditor, &keyPress);
    QApplication::sendEvent(&mainEditor, &keyRelease);
}

创建可复用的查找对话框

查找对话框的架构与设计

clipboard.png

查找对话框的界面布局

clipboard.png

查找功能的核心思想

  • 获取当前光标的位置并作为起始点
  • 向前(向后)目标第一次出现的位置
  • 通过目标位置以及目标长度在文本框中进行标记

查找算法流程

clipboard.png

MainWindow 与 FindDialog 之间的关系图

clipboard.png

文件: FindDialog.h

#ifndef FINDDIALOG_H
#define FINDDIALOG_H

#include <QDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QRadioButton>
#include <QPointer>
#include <QPlainTextEdit>

class FindDialog : public QDialog
{
    Q_OBJECT

protected:
    QGroupBox m_radioGrpBx;

    QGridLayout m_layout;
    QHBoxLayout m_hbLayout;

    QLabel m_findLbl;
    QLineEdit m_findEdit;
    QPushButton m_findBtn;
    QPushButton m_cancelBtn;
    QCheckBox m_matchChkBx;
    QRadioButton m_upwardBtn;
    QRadioButton m_downwardBtn;

    QPointer<QPlainTextEdit> m_pText;  // 注意这里,弱耦合设计!!

    void initControl();
    void connectSlot();

public slots:
    void onFindClicked();
    void onCancelClicked();

public:
    FindDialog(QWidget* parent = nullptr, QPlainTextEdit* pText = nullptr);
    void setPlainTextEdit(QPlainTextEdit* pText);
    QPlainTextEdit* getPlainTextEdit();
    bool event(QEvent* e);
    ~FindDialog();
};

#endif // FINDDIALOG_H

文件:FindDialog.cpp

#include "FindDialog.h"
#include <QEvent>
#include <QTextCursor>
#include <QMessageBox>

FindDialog::FindDialog(QWidget* parent, QPlainTextEdit* pText) : QDialog (parent, Qt::WindowCloseButtonHint | Qt::Drawer)
{
    initControl();
    connectSlot();

    setLayout(&m_layout);
    setFixedSize(450, 120);
    setWindowTitle("查找");

    setPlainTextEdit(pText);
}

void FindDialog::initControl()
{
    m_findLbl.setText("查找目标:");
    m_findBtn.setText("查找下一个(&F)");
    m_cancelBtn.setText("取消");
    m_matchChkBx.setText("区分大小写(&C)");
    m_radioGrpBx.setTitle("方向");
    m_upwardBtn.setText("向上(&U)");
    m_downwardBtn.setText("向下(&D)");
    m_downwardBtn.setChecked(true);

    m_hbLayout.addWidget(&m_upwardBtn);
    m_hbLayout.addWidget(&m_downwardBtn);
    m_radioGrpBx.setLayout(&m_hbLayout);

    m_layout.addWidget(&m_findLbl, 0, 0);
    m_layout.addWidget(&m_findEdit, 0, 1);
    m_layout.addWidget(&m_findBtn, 0, 2);

    m_layout.addWidget(&m_matchChkBx, 1, 0);
    m_layout.addWidget(&m_radioGrpBx, 1, 1);
    m_layout.addWidget(&m_cancelBtn, 1, 2);
}

void FindDialog::connectSlot()
{
    connect(&m_findBtn, SIGNAL(clicked()), this, SLOT(onFindClicked()));
    connect(&m_cancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
}

void FindDialog::setPlainTextEdit(QPlainTextEdit* pText)
{
    m_pText = pText;
}

QPlainTextEdit* FindDialog::getPlainTextEdit()
{
    return m_pText;
}

void FindDialog::onFindClicked()
{
    QString target = m_findEdit.text();

    if( (m_pText != nullptr) && (target != "") )
    {
        QString text = m_pText->toPlainText();
        QTextCursor c = m_pText->textCursor();
        int index = -1;

        if( m_downwardBtn.isChecked() )
        {
            index = text.indexOf(target, c.position(), m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);

            if( index >= 0 )
            {
                c.setPosition(index);
                c.setPosition(index + target.length(), QTextCursor::KeepAnchor);

                m_pText->setTextCursor(c);
            }
        }

        if( m_upwardBtn.isChecked() )
        {
            index = text.lastIndexOf(target, c.position() - text.length() - 1, m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);

            if( index >=0 )
            {
                c.setPosition(index + target.length());
                c.setPosition(index, QTextCursor::KeepAnchor);

                m_pText->setTextCursor(c);
            }
        }

        if( index < 0 )
        {
            QMessageBox msg(this);

            msg.setWindowTitle("记事本");
            msg.setText(QString("找不到 ") + "\"" + target + "\"");
            msg.setWindowFlag(Qt::Drawer);
            msg.setIcon(QMessageBox::Information);
            msg.setStandardButtons(QMessageBox::Ok);

            msg.exec();
        }
    }
}

void FindDialog::onCancelClicked()
{
    close();
}

bool FindDialog::event(QEvent* e)
{
    if( e->type() == QEvent::Close )
    {
        hide();      // 为了保持上一次用户的操作属性,进隐藏窗口

        return true;
    }

    return QDialog::event(e);
}

FindDialog::~FindDialog()
{

}

创建可复用的替换对话框

替换对话框的设计与实现

clipboard.png

替换对话框的界面布局

clipboard.png

替换流程图算法

clipboard.png

MainWindow 与 ReplaceDialog 之间的关系图

clipboard.png

文件:ReplaceDialog.h

#ifndef REPLACEDIALOG_H
#define REPLACEDIALOG_H

#include "FindDialog.h"

class ReplaceDialog : public FindDialog
{
    Q_OBJECT
protected:
    QLabel m_replaceLbl;
    QLineEdit m_replaceEdit;
    QPushButton m_replaceBtn;
    QPushButton m_replaceAllBtn;

    void initControl();
    void connectSlot();

protected slots:
    void onReplaceClicked();
    void onReplaceAllClicked();

public:
    ReplaceDialog(QWidget* parent = nullptr, QPlainTextEdit* pText = nullptr);
};

#endif // REPLACEDIALOG_H

文件:ReplaceDialog.cpp

#include "ReplaceDialog.h"

ReplaceDialog::ReplaceDialog(QWidget* parent, QPlainTextEdit* pText) : FindDialog (parent, pText)
{
    initControl();
    connectSlot();
}

void ReplaceDialog::initControl()
{
    m_replaceLbl.setText("替换为:");
    m_replaceBtn.setText("替换(&R)");
    m_replaceAllBtn.setText("全部替换(&A)");

    m_layout.removeWidget(&m_matchChkBx);
    m_layout.removeWidget(&m_radioGrpBx);
    m_layout.removeWidget(&m_cancelBtn);

    m_layout.addWidget(&m_replaceLbl, 1, 0);
    m_layout.addWidget(&m_replaceEdit, 1, 1);
    m_layout.addWidget(&m_replaceBtn, 1, 2);

    m_layout.addWidget(&m_matchChkBx, 2, 0);
    m_layout.addWidget(&m_radioGrpBx, 2, 1);
    m_layout.addWidget(&m_replaceAllBtn, 2, 2);

    m_layout.addWidget(&m_cancelBtn, 3, 2);

    setFixedSize(450, 170);
    setWindowTitle("替换");
}

void ReplaceDialog::connectSlot()
{
    connect(&m_replaceBtn, SIGNAL(clicked()), this, SLOT(onReplaceClicked()));
    connect(&m_replaceAllBtn, SIGNAL(clicked()), this, SLOT(onReplaceAllClicked()));
}

void ReplaceDialog::onReplaceClicked()
{
    QString target = m_findEdit.text();
    QString to = m_replaceEdit.text();

    if( (m_pText != nullptr) && (target != "") && (to != "") )
    {
        QString selText = m_pText->textCursor().selectedText();

        if( selText == target )
        {
            m_pText->insertPlainText(to);
        }

        onFindClicked();
    }
}

void ReplaceDialog::onReplaceAllClicked()
{
    QString target = m_findEdit.text();
    QString to = m_replaceEdit.text();

    if( (m_pText != nullptr) && (target != "") && (to != "") )
    {
        QString text = m_pText->toPlainText();

        text.replace(target, to, m_matchChkBx.isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive);

        m_pText->clear();

        m_pText->insertPlainText(text);
    }
}

Qt 中的调色板

  • QPalette 类包含了组件状态的颜色组
  • QPalette 对象包含了三个状态的颜色描述

    • 激活颜色组(Active)

      • 组件获得焦点使用的颜色搭配方案
    • 非激活颜色组(Inactive)

      • 组件失去焦点使用的颜色方案
    • 失效颜色组(Disabled)

      • 组件处于不可用状态使用的颜色方案

调色板是存储组件颜色信息的数据结构
组件外观所使用的颜色都位于调色板中

    QPalette p = mainEditor.palette();
    p.setColor(QPalette::Inactive, QPalette::Highlight, p.color(QPalette::Active, QPalette::Highlight));
    p.setColor(QPalette::Inactive, QPalette::HighlightedText, p.color(QPalette::Active, QPalette::HighlightedText));
    mainEditor.setPalette(p);

文本编辑器项目持续开发

行间跳转

  • 算法设计

    • 通过输入对话框获取目标行号
    • 查找换行符的位置计算目标行第一个字符的下标
    • 通过 QTextCursor 定位到目标行
void MainWindow::onEditGoto()
{
    bool ok = false;
    int ln = QInputDialog::getInt(this, "转到", "行号:", 1, 1, mainEditor.document()->lineCount(), 1, &ok,
                                  Qt::WindowCloseButtonHint | Qt::Drawer);
    if( ok )
    {
        QString text = mainEditor.toPlainText();
        QTextCursor c = mainEditor.textCursor();
        int pos = 0;
        int next = -1;

        for(int i=0; i<ln; i++)
        {
            pos = next + 1;
            next = text.indexOf('\n', pos);
        }

        c.setPosition(pos);

        mainEditor.setTextCursor(c);
    }
}

设置工具栏和状态栏的可见性

  • 实现思路

    • 通过 setVisble() 设置可见性
    • 更新界面上 QAction 对象的状态

      • 菜单栏中的 QAction 对象是否勾选
      • 工具栏中的 QAction 对象是否按下
void MainWindow::onViewToolBar()
{
    QToolBar* tb = toolBar();

    bool visible = tb->isVisible();

    tb->setVisible(!visible);

    findMenuBarAction("工具栏")->setChecked(!visible);
    findToolBarAction("工具栏")->setChecked(!visible);
}

void MainWindow::onViewStatusBar()
{
    QStatusBar* sb = statusBar();
    bool visible = sb->isVisible();

    sb->setVisible(!visible);

    findMenuBarAction("状态栏")->setChecked(!visible);
    findToolBarAction("状态栏")->setChecked(!visible);
}

自定义文本框中的字体和大小

  • 实现思路

    • 通过 QFontDialog 选择字体以及大小
    • 将 QFont 对象设置到文本编辑框
void MainWindow::FormatFont()
{
    bool ok = false;

    QFont font = QFontDialog::getFont(&ok, mainEditor.font(), this, "打印");

    if( ok )
    {
        mainEditor.setFont(font);
    }
}

自动换行

  • 获取当前编辑框的换行模式
  • 将模式进行反转后并进行设置
  • 更新对应 QAction 对象的状态
void MainWindow::FormatWrap()
{
    QPlainTextEdit::LineWrapMode mode = mainEditor.lineWrapMode();

    if( mode == QPlainTextEdit::NoWrap )
    {
        mainEditor.setLineWrapMode(QPlainTextEdit::WidgetWidth);

        findMenuBarAction("自动换行")->setChecked(true);
        findToolBarAction("自动换行")->setChecked(true);
    }
    else
    {
        mainEditor.setLineWrapMode(QPlainTextEdit::NoWrap);

        findMenuBarAction("自动换行")->setChecked(false);
        findToolBarAction("自动换行")->setChecked(false);
    }
}

打开外部文件

  • QDesktopServices 提供了一系列桌面开发相关的服务接口
  • 通过 QDesktopService 中的成员函数打开帮助文件
void MainWindow::onHelpManual()
{
    QDesktopServices::openUrl(QUrl("https://segmentfault.com/u/tiansong"));
}

程序中的配置文件

  • 应用程序在运行后都有一个初始化的状态
  • 一般而言: 程序的初始状态是最近一次运行退出前的状态

  • 解决思路
  • 程序退出前保存状态参数到文件(数据库)
  • 程序再次启动时读出状态参数并恢复

  • 状态参数的存储方式

    • 文件文件格式(XML, JSon, 等)
    • 轻量级数据库(Access, SQLite, 等)
    • 私有二进制文件

  • Qt 中的解决方案

    • 通过二进制数据流将状态参数直接存储于文件中
    • 优势:

      • 参数的存储和读取简单高效,易于编码实现
      • 最终文件为二进制文件,不易被恶意修改
  • 设计与实现

clipboard.png

clipboard.png

文件: AppConfig.h

#ifndef APPCONFIG_H
#define APPCONFIG_H

#include <QObject>
#include <QFont>
#include <QPoint>
#include <QSize>

class AppConfig : public QObject
{

protected:
    QFont m_editFont;
    QSize m_mainWindowSize;
    QPoint m_mainWindowPoint;
    bool m_isAutoWrap;
    bool m_isToolBarVisible;
    bool m_isStatusVisible;
    bool m_isVilid;

    bool restore();

public:
    explicit AppConfig(QObject *parent = nullptr);
    explicit AppConfig(QFont font, QSize size, QPoint point, bool isWrap, bool tbvisible, bool sbVisible, QObject *parent = nullptr);
    bool store();
    QFont editFont();
    QSize mainWindowSize();
    QPoint mainWindowPoint();
    bool isAutoWrap();
    bool isToolBarVisible();
    bool isStatusVisible();
    bool isVilid();
};

#endif // APPCONFIG_H

文件: AppConfig.cpp

#include "AppConfig.h"
#include <QFile>
#include <QDataStream>
#include <QApplication>

AppConfig::AppConfig(QObject *parent) : QObject(parent)
{
    m_isVilid = restore();
}

AppConfig::AppConfig(QFont font, QSize size, QPoint point, bool isWrap, bool tbvisible, bool sbVisible, QObject *parent) : QObject(parent)
{
    m_editFont = font;
    m_mainWindowSize = size;
    m_mainWindowPoint = point;
    m_isAutoWrap = isWrap;
    m_isToolBarVisible = tbvisible;
    m_isStatusVisible = sbVisible;

    m_isVilid = true;
}

bool AppConfig::restore()
{
    bool ret = true;
    QFile file(QApplication::applicationDirPath() + "/app.config");

    if( file.open(QIODevice::ReadOnly) )
    {
        QDataStream in(&file);

        in >> m_editFont;
        in >> m_mainWindowSize;
        in >> m_mainWindowPoint;
        in >> m_isAutoWrap;
        in >> m_isToolBarVisible;
        in >> m_isStatusVisible;

        file.close();
    }
    else
    {
        ret = false;
    }

    return ret;
}

bool AppConfig::store()
{
    bool ret = true;
    QFile file(QApplication::applicationDirPath() + "/app.config");

    if( file.open(QIODevice::WriteOnly) )
    {
        QDataStream out(&file);

        out << m_editFont;
        out << m_mainWindowSize;
        out << m_mainWindowPoint;
        out << m_isAutoWrap;
        out << m_isToolBarVisible;
        out << m_isStatusVisible;

        file.close();
    }
    else
    {
        ret = false;
    }

    return ret;
}

QFont AppConfig::editFont()
{
    return m_editFont;
}

QSize AppConfig::mainWindowSize()
{
    return m_mainWindowSize;
}

QPoint AppConfig::mainWindowPoint()
{
    return m_mainWindowPoint;
}

bool AppConfig::isAutoWrap()
{
    return m_isAutoWrap;
}

bool AppConfig::isToolBarVisible()
{
    return m_isToolBarVisible;
}

bool AppConfig::isStatusVisible()
{
    return m_isStatusVisible;
}

bool AppConfig::isVilid()
{
    return m_isVilid;
}
  • 值得思考的问题: 什么时候保存主窗口的状态数据?
  • 应用程序退出的过程

    • 收到关闭事件
    • 执行关闭事件处理函数
    • 主窗口从屏幕消失
    • 主窗口的析构函数执行
    • 。。。
  • 一般而言, 应用程序收到关闭事件时进行状态参数的保存
  • Qt 中的解决方案

    • 重写关闭事件处理函数
    • 在关闭事件处理函数中保存状态参数
void MainWindow::closeEvent(QCloseEvent *event)
{
    preEditChange();

    if( !m_isTextChanged )
    {
        QFont font = mainEditor.font();
        bool isWrap = (mainEditor.lineWrapMode() == QPlainTextEdit::WidgetWidth);
        bool tbVisible = (findMenuBarAction("工具栏")->isCheckable() && findToolBarAction("工具栏")->isChecked());
        bool sbVisible = (findMenuBarAction("状态栏")->isCheckable() && findToolBarAction("状态栏")->isChecked());
        AppConfig config(mainEditor.font(), size(), pos(), isWrap, tbVisible, sbVisible, this);

        config.store();

        QMainWindow::closeEvent(event);
    }
    else
    {
        event->ignore();
    }
}

命令行参数的应用

  • 命令行参数的应用 一

    • 传统应用方式

      • 在命令行启动 GUI 程序时传递参数

clipboard.png

  • 命令行参数的应用 二

    • 操作系统关联方式

      • 在文件被双击时,操作系统根据文件后缀选择应用程序
      • 将文件路径作为命令行参数启动应用程序

clipboard.png

int main(int argc, char *argv[])
{
    QApplication a(argc, argv); 
    int ret = -1;

    if( w != nullptr )
    {
      if( argc > 1 )
      {
            QFileInfo fi(argv[1]);

            if( fi.exists() )
            {
                w->openFile(argv[1]);
            }
      }

       w->show();

       ret = a.exec();

       delete w;
    }

    return ret;
}

应用程序的打包与发布

发布应用程序时的候选者

  • 调试版(debug): 开发阶段的可执行程序

    • 包含与调试相关的各种信息,体积巨大
    • 执行速度慢,支持断点调试
  • 发布版(release): 最终产品的可执行程序

    • 无任何冗余信息,体积小巧
    • 执行速度快,无法映射到源码调试

程序的依赖库

  • 可执行程序的正常运行需要外部库的支持
  • 因此,发布程序是必须保证所有的依赖库都存在

clipboard.png

  • Window 中可以使用 Depends 工具查看程序的依赖库
  • Linux 中可以使用 ldd 命令查看程序的依赖库

    • ldd 是 Linux 系统中一个脚本程序
    • 文件路径: /usr/bin/ldd

程序的环境依赖

  • 应用程序对执行环境可能存在依赖
  • 可能的依赖:

    • 环境变量,驱动程序,数据库引擎
    • Java 虚拟机, .net Framework
    • 。。。。。。
  • Window 下的环境部署

  • Linux 下的环境部署

    • 方式一:

      • 通过 ldd 命令确定程序的库依赖
      • 通过 Shell 脚本开发部署程序
    • 方式二:

      • 根据具体发行版开发专用部署程序(deb, rpm)

clipboard.png


附:为了降低模块间的耦合性,多处使用了QSharedPointer

仓库

以上内容参考狄泰软件学院系列课程,请大家保护原创!


TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧