【Qt】 3_Notepad 實例開發 - 可以使用

界面展現

圖片描述

倉庫php

核心概念

應用程序中的主窗口

  • 主窗口是與用戶進行長時間交互的頂層窗口
  • 程序的絕大多數功能直接由主窗口提供
  • 主窗口一般是應用程序啓動後顯示的第一個窗口
  • 裝個程序由一個主窗口和多個對話框組成

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;
}

主窗口中的狀態欄

狀態欄的概念和意義

  • 狀態欄是應用程序中輸出簡要信息的區域
  • 狀態欄通常位於主窗口的最底部
  • 狀態欄中的消息類型git

    • 實時消息,如:當前程序狀態
    • 永久消息,如:程序版本號,機構名稱
    • 進度消息,如:進度條提示,百分比提示

在 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 大類github

    • 文本文件: 文件內容是可讀的文本字符
    • 數據文件: 文件內容是直接的二進制數據

  • 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 相關的信號

  • 使用關鍵槽函數判斷數據狀態segmentfault

    • 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

倉庫

以上內容參考狄泰軟件學院系列課程,請你們保護原創!

相關文章
相關標籤/搜索