高仿富途牛牛-元件化(六)-炒雞牛逼的佈局記憶功能(序列化和反序列化)

朝十晚八發表於2019-06-27

一、佈局記憶

一款優秀的軟體,不僅僅要求功能強健、穩定性高和可靠的精準率,往往很多時候我們都需要去關注使用者介面是否友好,使用者操作是否順暢,軟體跨機器使用到底咋樣。

說起到怎麼讓使用者互動友好,這就是使用者體驗和視覺設計師的主場啦。這裡我就不多說了,今天主要是想說明一個問題--佈局記憶功能

現在客戶端軟體各式各樣,種類多了去了,但是不知道大家有沒有注意到有這麼一些互動上的細節。

  1. 使用過QQ的同學應該都比較清楚。我們在QQ使用時,除過第一次登入QQ軟體,其餘時間段登入QQ時,QQ的初始位置往往會是上一次退出時的位置

  2. windows資源管理器我們大家應該都經常在使用,不知道大家有沒有仔細觀察。我們修改了資源管理器視窗大小後,再次開啟資源管理器視窗時,新的視窗大小和我們之前修改後的視窗大小一樣。

  3. firefox郵件客戶端,大家都用過吧,也是會記憶視窗最後

  4. 還有一些工具軟體,比如說PicPick,選擇的使用模式會一直記錄

  5. QQ飛車是一款騰訊出的客戶端遊戲,他支援多種顯示模式,設定一次後,會一直生效,直到我們再次設定為止。

以上是我隨便寫的幾個資料記憶的事例,相信大家都不陌生。除過這些簡單的資料持久化以外,其實還有很多其他的事例,這裡就不一一例舉了。

今天我們主要是想給大家展示下我們負責視窗布局是怎麼進行佈局記憶的。

二、效果展示

視窗布局記憶如效果圖所示。

當我們通過主視窗關閉了軟體時,程式會自動把佈局資訊序列化成字串,然後進行儲存。

再次啟動軟體時,我們首先會去載入序列化的佈局資訊,然後進行解析佈局資訊,並構造我們的視窗,這個工程稱之為反序列化。

高仿富途牛牛-元件化(六)-炒雞牛逼的佈局記憶功能(序列化和反序列化)

三、重點回顧

1、視窗管理

之前我已經寫了好幾天文章都是講元件化相關的東西,其中有一篇文章高仿富途牛牛-元件化(五)-如何去管理炒雞多的小視窗主要是講解怎麼去管理過多的小視窗,主要是把建立的過程進行了封裝,讓外界使用起來更加的介面化。

本篇文章主要是講述佈局怎麼去記憶?記憶後又是怎麼去恢復?關於視窗建立和訊息通訊這裡我就不在去講解了。感興趣的同學可以翻看之前的文章,因為我的這個demo是在一直的維護,更新過程中,因此講到這篇文章的時候,之前的一些主題中的方式、方法可能已經發現了變化,如果有問題的歡迎留言。

2、頁籤TabButton

一個元件視窗中同時只允許一個頁籤被選中,選中另一個頁籤時,其他的頁籤都會被重置為非選中狀態。

TabButton是一個複雜的小視窗,支援同一個工具欄內拖拽,也支援多個工具欄之間拖拽。

3、子皮膚SubPanel

每一個元件視窗都包含有多個頁籤和多個SubPanel,其中SubPanel和頁籤時一對一的關係。

我們切換頁籤的時候,SubPanel也會跟隨者切換,而每一個SubPanel上都包含有不同的小視窗,這些小視窗都是由工具箱進行建立的。

工具箱這裡就不在多說了,看展示的效果圖,上邊就有一個工具箱視窗,當我們點選其上的工具按鈕時,就會在當前的SubPanel上建立一個對應的小視窗。

四、佈局記憶內容

首先我一直強調的是高仿富途牛牛-元件化,因此這裡記憶的內容我也是根據福牛的互動行為來記憶的,可能記憶的內容有下面這些,但也可能更多。

  1. 元件視窗個數
  2. 元件視窗位置和大小,包括層次關係
  3. 元件視窗關聯的工具箱是否顯示和其位置
  4. 工具欄的狀態,包括工具按鈕狀態,頁籤個數、順序、名稱和當前選中項
  5. 子皮膚上的小視窗
  6. 小視窗的層次關係、位置和大小

以上內容就是我們序列化時會儲存的資訊,但又不僅限於這些。

五、佈局資訊序列化

要讓佈局資訊持久化,那麼佈局資訊必然要被我們儲存到硬碟上,因此電腦上的記憶體資訊系統重啟後就會訊息。

好,那麼接下來就是考慮把佈局資訊寫入硬碟,這個時候我們就得找個合適的實際寫入時機,目前我寫入的時機是在關閉軟體的時候,但是這裡不建議大家也這麼搞,因此這回導致關節關閉有延遲,當我們有大量的資料需要寫入的時,可能會影響使用者體驗。

關於寫入時機選擇,不是本篇文章討論的主要內容,感興趣的可以自己去研究。

資料寫入時需要注意,給讀取資料時寫入一些標誌,否則讀取資料時如果包含一些迴圈,則不知道迴圈應該什麼時候結束。

1、流程

  1. 主元件視窗關閉時,開始序列化佈局資訊
  2. 首先寫入元件視窗個數,方便後期讀資料
  3. 工具欄按鈕狀態寫入
  4. 工具欄頁籤個數寫入
  5. 工具欄頁籤迴圈寫入
  6. 工具欄頁籤選中項index寫入
  7. 工具箱大小和位置寫入
  8. 迴圈子皮膚SubPanel
  9. 寫入SubPanel中所有小窗體資訊
  10. 小窗體資訊吸入:標題欄名稱、所屬組、視窗大小、位置等

2、主流程寫入

視窗資訊使用二進位制的方式寫入檔案,由於現在是demo階段,因此這裡為了方便測試,隨手寫了一個檔案路徑。

void TemplateLayout::SaveMainLayout()
{
    Q_ASSERT(m_pToolBar);

    QString path = "d:\\main.ttlayout";
    QFile file(path);
    if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
    {
        QDataStream in(&file);

        int count = templates.size();
        in << count;//儲存元件視窗個數

        //從最下面一級的窗體開始序列化
        for (int i = templates.size() - 1; i >= 0; --i)
        {
            TemplateLayout * widget = templates.at(i);
            widget->m_pToolBar->SaveLayout(in);
            in << QString("toolBar");//toolBar結束標誌

            widget->SaveToolBox(in);

            widget->m_pPanel->SaveLayout(in);
            in << QString("panels");//panel結束標誌
        }
    }
}

從最下面一級的元件窗體開始序列化,主要建立的時候,就是自下而上建立,視窗的z值就不存在問題。

序列化程式碼主體流程看起來就像上邊這樣,我們使用QDataStream來進行二進位制資訊的寫入。

在整個寫入的過程中,我們使用了一個QDataStream物件,並把檔案作為他的輸入裝置。

這裡需要注意一點,我們不能在函式呼叫過程中使用多個QDataStream,把每個視窗的佈局資訊都儲存到一個QByteArray中去。因為QDataStream內部在儲存資料時,會在末尾加上4個位元組的結束符,這樣我們在多層巢狀寫資料時,雖然沒有問題,但是讀資料時就會出現問題,這個問題我也是查了好久就通過除錯程式碼發現的

3、標籤頁寫入

前邊我們也說了,我們整個的寫入過程都使用了一個QDataStream,內部視窗的寫入都是使用了最外層的QDataStream,這裡從引數我們也可以看得出來。

標籤頁寫入方式和之前的模式差不多,主要是儲存的資料不同,這裡主要存放了3種資訊:標籤頁數量、標籤頁名稱和選中項下標

void DragTabWidget::SaveLayout(QDataStream & in) const
{
    Q_ASSERT(m_pTabLayout);
    in <<  m_buttonMaps.size();//記錄button個數
    int selectedIndex = 0;
    int buttonIndex = 0;
    for (int index = 0; index < m_pTabLayout->count(); ++index)
    {
        if (TabButton * desButton = dynamic_cast<TabButton *>(m_pTabLayout->itemAt(index)->widget()))
        {
            in << desButton->Text();
            if (desButton->IsSelected())
            {
                selectedIndex = buttonIndex;
            }
            ++buttonIndex;
        }
    }
    in << selectedIndex;//記錄選中按鈕
}

4、小視窗寫入

小視窗寫入時,首先寫入了的是標題欄的資訊,然後在寫入視窗自身的位置、大小和視窗型別

這裡需要重點提下視窗型別,這個資訊很重要。當我們反序列化的時候,需要根據這個型別來進行建立視窗

void SmallWidget::SaveLayout(QDataStream & in) const
{
    QPoint pos = this->pos();//儲存位置
    QSize size = this->size();//儲存大小
    SubWindowNormalType type = GetSmallType();//儲存視窗型別

    m_pTitle->SaveLayout(in);
    
    in << pos;
    in << size;
    in << (int)type;
}

5、其他

序列化的整個過程基本都是一樣的套路,主要就是使用QDataStream物件把佈局資訊以二級制的形式寫入到硬碟檔案中。

其他的佈局資訊寫入方式大豆差不多,這裡就不一一列出。

六、佈局資訊反序列化

說完序列化後,接下來就是我們的反序列化的過程了。

反序列化就是序列化的相反過程,主要是我們需要寫入正確的資訊,然後按寫入時的順序進行讀取佈局資訊即可

1、流程

  1. 啟動程式時,開啟佈局檔案
  2. 讀出元件視窗個數
  3. 讀取工具欄按鈕狀態
  4. 初始化頁籤,這個時候SubPanel也會被初始化
  5. 初始化頁籤選中項
  6. 讀取工具箱大小和位置
  7. 初始化各子皮膚上的小視窗
  8. 迴圈第三步

2、反序列化主流程

反序列化就是序列化的逆序,不過這裡需要注意的一個地方就是,我們序列化的時候,主視窗時最後儲存的,因此反序列化的時候,主視窗也是最後才進行初始化的。

注意程式碼中的if (i == count - 1)這個if判斷,就是處理主視窗初始化。

void TemplateLayout::RestoreLayout()
{
    QString path = "d:\\main.ttlayout";
    QFile file(path);
    if (file.open(QIODevice::ReadOnly))
    {
        QDataStream out(&file);

        int count;
        out >> count;//儲存元件視窗個數
        
        for (int i = 0; i < count; ++i)
        {
            TemplateLayout * widget = nullptr; 
            if (i == count - 1)//最後一個是主視窗
            {
                widget = this; 
            }
            else
            {
                widget = new TemplateLayout;
                widget->setWindowFlags(Qt::FramelessWindowHint);
                widget->m_pToolBar->SetMoveable(true);
                widget->SetIsMajor(false);
                widget->show();
            }
            widget->m_pToolBar->LoadLayout(out);

            QString toolSign;
            out >> toolSign;//toolBar結束標誌
            Q_ASSERT(toolSign == "toolBar");

            widget->LoadToolBox(out);

            widget->m_pPanel->LoadLayout(out);

            QString panelSign;
            out >> panelSign;//panel結束標誌
            Q_ASSERT(panelSign == "panels");
        }
    }
}

3、工具欄按鈕

讀取工具欄按鈕的資訊,並進行初始化。

工具欄按鈕主要是有兩個

  1. 小工具視窗是否開啟
  2. 磁力吸附特性是否啟用。

程式碼中toolBoxChecked就是表示工具箱按鈕是否被選中,magneticChecked表示吸力吸附按鈕是否被選中

void DragToolBar::LoadLayout(QDataStream & out)
{
    bool toolBoxChecked, magneticChecked;
    out >> toolBoxChecked;
    out >> magneticChecked;

    Q_ASSERT(m_pToolBoxAct);
    m_pToolBoxAct->setChecked(toolBoxChecked);
    m_pToolBoxAct->triggered(toolBoxChecked);

    Q_ASSERT(m_pMagneticAct);
    m_pMagneticAct->setChecked(magneticChecked);
    m_pMagneticAct->triggered(magneticChecked);

    Q_ASSERT(m_pDragTab);
    m_pDragTab->LoadLayout(out);
}

4、初始化標籤頁

載入工具欄上標籤頁,分3個步驟

  1. 讀取標籤頁個數
  2. 迴圈讀取所有標籤頁
  3. 讀取選中的標籤頁下標

根據讀取到的資訊初始化工具欄。

void DragTabWidget::LoadLayout(QDataStream & out)
{
    int count;
    out >> count;
    QStringList titles;

    while (count-- > 0)
    {
        QString title;
        out >> title;
        titles.append(title);
    }
    
    int selectedIndex = 0;
    out >> selectedIndex;

    TabButton * selected = nullptr;
    for (int i = 0; i < titles.size(); ++i)
    {
        QString title = titles.at(i);
        UpdateMaxOrder(title);
        TabButton * button = AddNewButton(title);
        if (i == selectedIndex)
        {
            selected = button;
        }
    }

    if (selected)
    {
        ButtonClicked(selected->GetID());
    }
}

5、子皮膚初始化

在佈局資訊序列化小結中,我們講述了子皮膚中的小視窗在寫入資訊時,寫入了視窗的型別type,這個時候我們就會發現這個type真的太重要了

看如下程式碼,我們讀出了小視窗的type值,然後使用SmallFactory工廠的CreateWidget方法建立了小視窗,程式碼看起來是不是還是比較流暢的。

除過視窗型別外,還包括了視窗標題欄名稱、所屬組、位置、大小等資訊

void SubContentWidget::LoadeLayout(QDataStream & out)
{
    QString titleName, groupName;
    QPoint pos;
    QSize size;
    int type;
    int count;
    out >> count;

    while (count-- > 0)
    {
        out >> titleName;
        out >> groupName;

        out >> pos;//儲存位置

        out >> size;//儲存大小

        out >> (int)type;//儲存視窗型別

        SmallWidget * smallWidget = SmallFactory::GetInstance()->CreateWidget(SubWindowNormalType(type), this);
        AddSmallWidget(smallWidget);

        smallWidget->SetWindowTitle(titleName);
        if (groupName.isEmpty() == false)
        {
            smallWidget->SetToolText(STT_GROUP, groupName);
        }
        smallWidget->move(pos);
        smallWidget->resize(size);
        smallWidget->show();
    }
}

6、其他

反序列化的整個過程基本都是一樣的套路,主要就是使用QDataStream物件把佈局資訊以二級制的形式讀入到記憶體中。

其他視窗的反序列化操作基本類似,這裡就不一一列出。

七、相關文章

高仿富途牛牛-元件化(一)-支援頁籤拖拽、增刪、小工具

高仿富途牛牛-元件化(二)-磁力吸附

高仿富途牛牛-元件化(三)-介面美化

高仿富途牛牛-元件化(四)-優秀的時鐘

高仿富途牛牛-元件化(五)-如何去管理炒雞多的小視窗

C++序列化物件


以上的內容,基本上就是本篇文章的內容所有內容啦!序列化和反序列化功能基本完成,希望可以幫到大家。




很重要--轉載宣告

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用連結的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。


相關文章