Qt開發系列3——Qt中的核心技術1

簡介

這里簡單介紹Qt的一些核心機制,具體參見Qt文檔。

主要包含內容:

  • Qt的信號和槽,以及事件機制
  • Qt Object Model
  • Qt Embedded for linux簡介
  • 事件機制
  • 顯示機制
  • Qt的通信機制
  • Qt的插件系統(機制)
  • Qt內存管理機制
  • Qt的Model/View編程模式
  • 繪制系統

具體如下。

Qt的信號和槽,以及事件機制

信號和槽提供了一種在一個對象中,直接調用另一個對象任意成員函數的機制。類似回調,但比直接調用回調函數靈活(例如會自動處理虛函數調用),相應的調用的性能也有一定下降(開銷很小,比new和delete操作小)。

Qt Object Model

需要注意兩點:Qt對標準C++通過此模型進行了一定的擴展;Qt中對象的賦值和克隆完全不同,后者所做工作更多。

Meta-Object System

此特性通過Qt的moc工具,為每一個使用Qt特性的類生成一個moc對象來實現。它包含了Qt對C++的許多擴展性能的處理和實現。如:

  • 信號和槽的機制
  • 動態添加類屬性的機制
  • 不通過RTTI獲取類名的機制
  • 獲取繼承關系的機制等。

使用此特性的方法很簡單,只需在相應的Qt類中繼承QObject,并且在開始聲明Q_OBJECT宏。編譯時,需要用moc生成相應的moc對象實現的cpp文件,并鏈接;但是使用qmake工具的話,會自動生成Makefile,不用手動去做。

Qt Embedded for linux簡介

相對Qt的桌面程序,QTE自己提供了一個輕量級的窗口管理系統,其應用程序直接操作framebuffer,而不使用Xwindow系統這樣的桌面管理程序,可以節省內存。也可以使用VNC遠程桌面控制協議,運行應用程序。

(1)服務端和客戶端

QtEmbedded應用啟動時,需要一個服務端,或者是一個已有的服務端,或者應用程序本身作為一個服務端啟動。任何一個QTE程序都可以成為服務端程序(通過啟動選項中的-qws指定,或者在編碼中指定),啟動好一個服務端之后,后續的QTE程序都將作為客戶端的角色運行。

服務端主要負責管理鼠標鍵盤輸入、顯示輸出、屏幕保護以及光標顯示等內容(類似圖形系統中的桌面管理系統),而客戶端則借助服務端提供的服務,完成特定的應用程序功能。所有系統產生的事件(例如鍵盤鼠標事件),都會傳遞給服務端,然后分發給特定的客戶端處理。

運行的應用程序會不斷地通過添加和減少widgets來更改屏幕的外觀。服務端會在相應的QWSWindow 類對象中維護沒一個頂層窗口的信息。當服務端接受到事件,它會通過詢問它的頂層窗口棧,來找到包含事件位置的窗口。每個窗口又可以知道創建它自身的客戶應用程序。服務端會在最后將封裝成QWSEvent類對象的事件轉發給相應的應用程序(客戶端)。

輸入法使用一個介于服務端和客戶端的filter來實現。我們繼承QWSInputMethod來實現自定義的輸入法,再使用服務端的setCurrentInputMethod()來安裝它。另外,也可使用QWSServer::KeyboardFilter類來實現一個全局的底層的filter,對key events進行過濾處理,這樣可以用于一些特殊目的,無需為所有應用程序添加一個filter(例如通過一個按鈕進行高級電源管理)。

(2)通信

server通過unix域套接字和client進行通信。客戶端和服務端通信時,使用的是QCopChannel類,QCOP是一個在不同channel可以進行多對多通信的協議。一個channel通過一個名字標識,任何程序都可以偵聽這個channel,QCOP協議可以允許客戶端在相同地址空間也可在不同的進程間通信。

(3)指針輸入

QTE服務端啟動時,使用QT的插件機制,將鼠標驅動加載。鼠標驅動接受設備產生的鼠標事件,并將其封裝成 QWSEvent 類,傳遞給服務端。

QT默認提供了一個鼠標驅動,我們可以繼承QWSMouseHandler實現自定義的鼠標驅動。 QMouseDriverFactory默認會在服務端運行時,自動檢測到該驅動,并將其加載。

除通常的鼠標輸入外,QTE提供了一個calibrated mouse handler。當系統設備無法具有設備和屏幕的固定映射,以及有噪聲事件時(例如觸摸屏),使用QWSCalibratedMouseHandler作為基類來實現使用。

(4)字符輸入

QTE服務端啟動時,使用QT的插件機制,將鍵盤驅動加載。鍵盤驅動接受設備產生的鍵盤事件,并將其封裝成 QWSEvent 類,傳遞給服務端。

QT默認提供了一個鍵盤驅動,我們可以繼承 QWSKeyboardHandler實現自定義的鍵盤驅動。QKbdDriverFactory默認會在服務端運行時,自動檢測到該驅動,并將其加載。

(5)顯示輸出

每個客戶端默認會將它的widgets和decorations提交到內存,同時服務端將相應內存拷貝到設備的framebuffer。

當客戶端接收到一個可以更改它widgets的事件時,應用程序就會更新它內存緩存的相應部分。

decoration在客戶應用程序啟動時通過QT插件系統加載,可以通過繼承QDecoration來自定義一個decoration插件。默認QDecorationFactory會自動檢測到這個插件并加載給應用程序。我們可以用QApplication::qwsSetDecoration()函數來為應用程序指定一個給定的decoration。

事件機制

事件機制主要用于對Qt類的實現,與信號和槽的區別是它用于類自身使用而非為其它對象調用提供接口(例如在事件處理函數中發送信號)。由Qt自身的事件循環機制維護。

(a)事件循環

Qt程序啟動后,通過QApplication::exec()進入事件循環,對事件進行派發處理。

該循環大致如下:

while ( !app_exit_loop )
{
    while( !postedEvents ) { processPostedEvents() }
    while( !qwsEvnts ){ qwsProcessEvents();   }
    while( !postedEvents ) { processPostedEvents() }
}

先處理Qt事件隊列中的事件, 直至為空. 再處理系統消息隊列中的消息, 直至為空, 在處理系統消息的時候會產生新的Qt事件, 需要對其再次進行處理.

事件的派發處理通過QApplication::notify()進行。

調用QApplication::sendEvent的時候, 消息會立即被處理,是同步的. 實際上QApplication::sendEvent()是通過調用QApplication::notify(), 直接進入了事件的派發和處理環節.

(b)派發處理

假設Qt程序(QApplication)的QWidget發生事件QEvent,那么處理的次序是:

  1. QApplication::notify()對QEvent進行派發
  2. 在QApplication::notify()中,用安裝在QApplication上的事件過濾器處理
  3. 在QApplication::notify()中,調用QObject::event()對事件進行處理
  4. 在QObject::event()中,用安裝在QWidget上的事件過濾器處理
  5. 在QObject::event()中,調用QWidget自己的XXXEvent函數進行處理

(c)事件轉發

事件在QWidget中處理后,通過返回true或false來標志是否處理完。處理完則不轉發,否則向上依次轉發給父窗口,直至被處理或到頂層窗口。

顯示機制

Qt默認情況,客戶端提交顯示widgets的相關請求到內存,服務端會遍歷所有客戶端的頂層窗口確認顯示區域,將所有客戶端的顯示相關請求從內存中拷貝到屏幕上,期間,會使用到Qt的screen驅動(screen驅動的加載涉及到Qt的插件機制)。但是對于已知硬件信息的時候,客戶端可以直接來操作和控制硬件而不用借助服務端(這在嵌入式系統中也是很常見的),后面會介紹兩種直接和硬件交互的方法。另外,我們還可創建自己的顯示機制,充分利用硬件性能。

(1)關于screen驅動顯示

screen驅動會根據一個和顯示區域有交疊的所有頂層的窗口列表,來確認更新顯示的內存。每個頂層窗口都有一個QWSWindowSurface類來表示其繪制區域,screendriver根據這些類對象來獲取相應的內存塊指針。最后,screen驅動會對這些內存塊進行合成,并將更新的顯示區域提交到framebuffer顯示出來。

Qt提供的顯示驅動

主要有:

  • Linux framebuffer:直接在linux系統的framebuffer上進行顯示相關操作。
  • the virtual framebuffer:通過qvfb程序模擬出虛擬的framebuffer設備,在其中進行顯示相關操作。
  • transformed screens:和屏幕旋轉相關的操作。
  • VNC servers:服務端會啟動一個小型的vnc服務,網絡上的其它機器通過vnc方式訪問其內容,服務端通過vnc進行顯示。
  • multi screens:同時支持多種顯示驅動的驅動。

需在編譯QTE時,配置好需要使用的驅動。

指定顯示驅動

可通過環境變量"QWS_DISPLAY",或者命令行選項"-display"指定通過哪種驅動顯示。

例如:啟動服務后,

  • 通過環境變量:

    #export QWS_DISPLAY="VNC:0"
    #myApplication&
    
  • 通過命令行選項:

    #myApplication -display "VNC:0"
    

兩種方式均表示使用vnc進行顯示,其它具體顯示參數需參見文檔。

插件自定義顯示驅動

我們可以通過繼承QScreen類以及創建一個繼承自QScreenDriverPlugin類的插件,來使用自己定義的顯示驅動插件。QScreenDriverFactory類默認會自動檢測到這個插件,并在運行時將它加載至服務程序。

(2)關于客戶端直接顯示

前面提到的,客戶端可以直接來操作和控制硬件而不用借助服務端來顯示的兩種方式:

第一種方式

設置 Qt::WA_PaintOnScreen 屬性(如果所有的widget都這樣顯示,我們可以設置環境變量 QT_ONSCREEN_PAINT )。設置后,應用程序會直接將其widget顯示到屏幕上,并且其相關的顯示區域將不會被screen驅動修改(除非有一個持有更高窗層焦點的程序在同樣的區域有提交窗口更新相關的請求)。

第二種方式

使用 QDirectPainter 。這樣可以完全控制一處預先保留的framebuffer區域(通過持有一塊framebuffer的指針),screen驅動也再也無法修改這片區域了。但是如果當前屏幕有子屏幕,我們還是需要借助screen驅動的相關函數來獲取正確的屏幕,獲取當前的屏幕,以及恢復之前的framebuffer指針。

(3)加速顯示

對QTE來說,繪制顯示是一個純軟件實現,為充分利用特殊硬件的顯示加速特性,我們可以自己添加更高性能的圖形繪制驅動。

前面說過,客戶端使用Qt的繪圖系統將每個窗口提交給一個window surface對象,然后將其保存到內存,screen驅動會訪問這些內存并將這些surface合并,并顯示出來。

為了添加一個加速的圖形顯示驅動,我們需要自己創建一個screen,一個圖形繪制引擎,一個支持繪制引擎的圖形繪制設備,一個支持圖形設備的窗口的surface,并且使screen可以識別這個surface。具體需參見"accelerated graphics driver"的文檔。

Qt的通信機制

Qt為Qt應用程序提供以下通信機制

(1)D-Bus

QtDBus模塊是一個unix庫,可以使用它基于D-Bus協議進行通信。它將Qt的信號和槽的機制擴展到IPC層,允許一個進程的信號可以連接另外一個進程的槽。

(2)TCP/IP

跨平臺的QtNetwork模塊提供的類便于網絡編程和移植。它提供了高層類(如Http,QFtp)等,可以用于和特定應用層協議通信;也提供了低層類(如QTcpSocket,QTcpServer,QSslSocket)來實現協議。

(3)Shared Memory

跨平臺的共享內存類,QSharedMemory提供了訪問操作系統共享內存的實現。它允許多線程和進程安全的訪問共享內存段。另外,QSystemSemaphore可以對系統共享資源的訪問以及進程通信進行控制。

(4)Qt COmmunications Protocol (QCOP)

QCopChannel類實現了客戶進程通過有名channels傳輸消息的協議。它只能用于Qt for Embedded Linux,類似QtDBus,QCOP將Qt的信號和槽機制擴展到IPC層次,允許一個進程的信號可以連接另外一個進程的槽,但是與QtDBus不同的是QCOP不依賴第三方庫。

Qt的插件系統(機制)

Qt提供兩組API用于創建插件:

  1. 高層的API:用于擴展Qt本身,比如自定義的數據庫驅動,文本解碼插件,風格插件等。
  2. 低層的API:用于擴展應用程序本身。

高層API實際建立在低層API之上。

1.擴展Qt本身的高層插件

編寫一個用于擴展Qt本身的高層插件,主要做的就是:繼承一個特定類型的插件基類、實現一些函數、再增加一個宏。

編好的插件存放在特定的目錄下,Qt會自動找到并加載。此方式創建的插件類型固定,每個類型對應一個$QTDIR/plugins目錄下的子目錄(也是Qt插件系統自動搜索的路徑之一),插件就存放于其中。

Qt加載插件的路徑搜索規則,以及添加插件的方法,具體請參見文檔。大致如下:

  • 將當前可執行程序路徑作為插件搜索根目錄,搜索特定類型的插件(如styles),可使用QCoreApplication::applicationDirPath()獲得此根路徑。
  • 將QLibraryInfo::location(QLibraryInfo::PluginsPath)獲得的路徑作為插件搜索根目錄,搜索特定類型的插件(如styles),一般為:QTDIR/plugins。
  • 應用程序可使用 QCoreApplication::addLibraryPath()追加額外的搜索路徑根目錄。
  • 編寫一個qt.conf來替換Qt內部硬編碼后確定的路徑,此文件存在于/qt/etc/qt.conf(根據系統有所不同),以及當前程序執行路徑。
  • 另外,啟動程序前,若指定 QT_PLUGIN_PATH,則使用此變量中的路徑來搜索插件。

每種類型的插件有其不同的實現規則,基本上是繼承相應的插件基類(如QStylePlugin),實現一些特定的函數,最后用Q_EXPORT_PLUGIN2宏進行相應聲明。

一般使用插件的方式是將其直接包含并編譯到應用程序中,或者將其編譯成動態庫,并鏈接。如果想要讓插件可加載,那么就按照前面的規則,在搜索目錄的相應位置為插件建立一個目錄,并將插件拷貝進去。

2.擴展應用程序本身的低層插件

不僅是Qt本身,Qt應用程序也可通過插件擴展。應用程序通過QPluginLoader來檢測和加載插件。應用程序的插件不僅限于Qt插件的那幾種類型(如data base、style等),可以任意,較Qt的插件,靈活性更大。

創建一個應用程序插件,大致包含下面的步驟:

  • 聲明一個只包含純虛函數接口的類,用于描述插件功能供插件實現。
  • 使用Q_DECLARE_INTERFACE()宏將上述接口通知給Qt的meta-object系統。
  • 在應用程序中使用QPluginLoader來加載插件。
  • 使用qobject_cast()檢測插件是否實現了指定接口。

編寫插件包含如下步驟:

  • 聲明一個插件類,繼承自QObject和之前的接口類。
  • 使用Q_INTERFACES()宏將上述接口通知給Qt的meta-object系統。
  • 使用Q_EXPORT_PLUGIN2()宏將插件導出。
  • 使用合適的.pro文件編譯插件。

3.插件加載與檢測

  • 高(主和次)版本Qt編譯鏈接的插件,不能被低(主和次)版本Qt加載。

    例如: 4.5.3編譯的插件,不能被4.5.0加載。

  • 低主版本號Qt編譯鏈接的插件不能被高主版本號的Qt庫加載。

    例如:

    Qt 4.3.1 不會加載Qt 3.3.1編譯鏈接的插件。

    Qt 4.3.1 會加載Qt 4.3.0 and Qt 4.2.3編譯鏈接的插件。

  • Qt庫和所有插件用一個聯編關鍵字來聯編。如果Qt庫的和插件的聯編關鍵字匹配則加載,否則不加載。

    編譯插件來擴展應用程序時,需確保插件和應用程序用同樣的配置。

    如果應用程序是release模式編譯的,那么插件也要是release模式。

    若將Qt配置為debug和release模式都編譯,但只在release模式下編譯應用程序,就要確保你的插件也是在release模式下編譯的。

    缺省的,若Qt的debug編譯可用,插件就只在debug模式下編譯。要強制插件用release模式編譯,要在工程中添加:CONFIG += release

這能確保插件兼容應用程序中所用的庫版本。更多內容,參見官方文檔。

注:個人理解,Qt驅動一般就是指Qt插件,其實現根據底層被操作設備而不同,但對上提供統一的接口。

Qt內存管理機制

所謂Qt內存管理機制,是一種半自動的垃圾回收機制,所有繼承于QObject的類,并設置了parent(在構造時,或用setParent函數,或parent的addChild相關信息),在parent被delete時,這個parent的相關所有child都會自動delete,不用用戶手動處理。

程序通常最上層會有一個根的QOBJECT,就是放在setCentralWidget()中的那個QOBJECT,這個QOBJECT在 new的時候不必指定它的父親,因為這個語句將設定它的父親為總的QAPPLICATION,當整個QAPPLICATION沒有時它就自動清理,所以也無需清理(這里QT4和QT3有不同,QT3中用的是setmainwidget函數,但是這個函數不作為里面QOBJECT的父親,所以QT3中這個頂層的QOBJECT要自行銷毀)。

我們需要注意如下容易出錯的三種情況:

(1)child被單獨釋放

parent是用一個數組來保存childs的指針的,當一個child被銷毀時,parent會知道的。child的析構函數會調用parent并把parent的指針數據中自己對數的值改為0,那么最后是0的指不管多少次都無所謂了。

但是當一個QOBJECT正在接受事件隊列中途就被你DELETE掉了,會出現問題,所以QT中建議不要直接DELETE掉一個 QOBJECT,如果一定要這樣做,要使用QOBJECT的deleteLater()函數,它會讓所有事件都發送完一切處理好后馬上清除這片內存,而且就算調用多次的deleteLater也不會有問題(具體可查看deleteLater在api文檔中的解釋)。

(2)非new出來的child的釋放

parent不區別它的child是不是new出來的,只要是它的child,它在銷毀時就直接delete。因此如下代碼是有錯誤的:

{
    QObject*parent=new QObject(0);
    QObject child(parent);
    delete parent;
}

上面代碼中delete parent時,會對child進行delete,而child不是new的,導致出錯。在正確的QT開發中,頂級的patent一般是在main函數中,而patent生命周期一般都會比child長,而上述代碼中,parent的生命周期比child短。

(3)在parent范圍外持有childs的指針

在parent釋放后,其child不知道自己被delete了,此時child的指針就是野指針。Qt不建議在一個parent的范圍之外持有對childs的指針,這樣就不會出現前面那樣野指針的問題了。但是非要在parent外持有child的指針,那么Qt推薦使用QPointer,QPointer相當于一個智能指針,不用智能指針前的代碼如下:

{
    QObject*parent=new QObject(0);
    QObject*child=new QObject(parent);
    delete parent;
    child->...
}

這里第4步"child->"會出錯,因為其parent已經在前面"delete parent"時也將它釋放了。應該這樣:

{
    QObject*parent=new QObject(0);
    QObject*child=new QObject(parent);
    QPointerp=child;
    delete parent;
    if(p.isNull()){
        p->...
    }
}

這里使用QPointer,可以判斷出child是否被釋放。

Qt的Model/View編程模式

Qt 4使用model/view結構來管理數據與表示層的關系。這種結構將顯示與數據分離,給開發人員帶來更大的彈性來定制數據項的表示。

Model-View-Controller(MVC), 是從Smalltalk發展而來的一種設計模式,常被用于構建用戶界面。MVC 由三種對象組成。

  1. Model是應用程序對象,
  2. View是它的屏幕表示,
  3. Controller定義了用戶界面如何對用戶輸入進行響應。

在MVC之前,用戶界面設計傾向于三者揉合在一起,MVC對它們進行了解耦,提高了靈活性與重用性。

假如把view與controller結合在一起,結果就是model/view結構。這個結構依然是把數據存儲與數據表示進行了分離,它與MVC都基于同樣的思想,但它更簡單一些。這種分離使得在幾個不同的view上顯示同一個數據成為可能,也可以重新實現新的view,而不必改變底層的數據結構。為了更靈活的對用戶輸入進行處理,引入了delegate這個概念。它的好處是,數據項的渲染與編程可以進行定制。

許多便利類都源于標準的view類,它們方便了那些使用Qt中基于項的view與table類,它們不應該被子類化, 它們只是為Qt 3的等價類提供一個熟悉的接口。QListWidget,QTreeWidget,QTableWidget,它們提供了如Qt 3中的QListBox, QlistView,QTable相似的行為。這些類比View類缺少靈活性,不能用于任意的models,推介使用model/view的方法處理數據。

Qt采用Model/View的方式,主要相關類可以被分成上面所提到的三組:models,views,delegates。

  1. model,與數據源通訊,并提供接口給結構中的別的組件使用。通訊的性質依賴于數據源的種類與model實現的方式;
  2. view,從model獲取model indexes,通過model indexes,view可以從model數據源中獲取數據并組織;
  3. delegate,會在標準的views中對數據項進行進一步渲染或編輯,當某個數據項被選中時,delegate通過model indexes與model直接進行交流。

models,views,delegates之間通過信號,槽機制來進行通訊。

1.Models

所有的item models都基于QAbstractItemModel類,這個類定義了用于views和delegates訪問數據的接口。數據本身不必存儲在model,數據可被置于一個數據結構或另外的類,文件,數據庫,或別的程序組件中。QT提供了一些現成的models用于處理數據項:

  • QStringListModel 用于存儲簡單的QString列表。
  • QStandardItemModel 一個多用途的model,可用于表示list,table,tree views所需要的各種不同的數據結構,這個數據每項都可以包含任意數據。
  • QDirModel 提供本地文件系統中的文件與目錄信息。
  • QSqlQueryModel, QSqlTableModel,QSqlRelationTableModel用來訪問數據庫。

假如這些標準Model不滿足需要,我們可以子類化QAbstractItemModel,QAbstractListModel或是QAbstractTableModel來定制自己所需的數據。

(1)ModelIndex

通過model index,可以引用model中的數據項,而不必關注底層的數據結構。

Views和delegates都使用indexes來訪問數據項,然后再顯示出來。這使得數據存儲與數據訪問分開,只有model需要了解如何獲取數據,

model index需要關注關于model的三個屬性:行數,列數,父項的model index。

另外,有時model會重新組織內部的數據結構,所以保存臨時的model indexes可能會失效,所以這時應該創建一個長期的model index保存,這個引用會保持更新。

臨時的model indexes由QModelIndex提供,而具有持久能力的model indexes則由QPersistentModelIndex提供。

(2)Model role

model中的項可以作為各種角色來使用,這意味著在不同的環境下,Model會提供不同的數據(給View或Delegate)。

例如Qt::DisplayRole用于訪問一個字符串,設置此角色后,數據項會作為文本在view中顯示。標準的角色在Qt::ItemDataRole中定義。我們可以通過指定model index與角色來獲取我們需要的數據。

一個通過Model Index 和role訪問Model項的例子:

QDirModel *model = new QDirModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
int numRows = model->rowCount(parentIndex);//獲取model的尺寸
for (int row = 0; row < numRows; ++row)
{
    QModelIndex index = model->index(row, 0, parentIndex);//樹形目錄結構需要row和parentIndex信息定位特定數據項
    tring text = model->data(index, Qt::DisplayRole).toString();//指定role為Qt::DisplayRole,可獲取相應字符串數據。
    // Display the text in a widget.
}

(3)自己設計Model的例子:

假設我們實現一個自己的model, 用來顯示字符串列表。

類聲明如下:

class MyStringListModel : public QAbstractListModel
{
    Q_OBJECT


    public:
        MyStringListModel(const QStringList &strings, QObject *parent = 0): QAbstractListModel(parent), stringList(strings) {}
        int rowCount(const QModelIndex &parent = QModelIndex()) const;
        QVariant data(const QModelIndex &index, int role) const;
        QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
        Qt::ItemFlags flags(const QModelIndex &index) const;
        bool setData(const QModelIndex &index,const QVariant &value, int role);


    private:
        QStringList stringList;
};

除了構造函數,我們僅需要實現兩個函數:rowCount()返回model中的行數,data()返回與特定model index對應的數據項。具有良好行為的model也會實現headerData(),它返回tree和table views需要的,在標題中顯示的數據。

因為這是一個非層次結構的model,我們不必考慮父子關系。假如model具有層次結構,我們也應該實現index()與parent()函數。

每個函數實現:

int MyStringListModel::rowCount(const QModelIndex &parent) const
{//數據的長度即stringList長度
    return stringList.count();
}

QVariant MyStringListModel::data(const QModelIndex &index, int role) const
{//獲取數據,這里只有一個角色:Qt::DisplayRole
    if (!index.isValid())
        return QVariant();


    if (index.row() >= stringList.size())
        return QVariant();


    if (role == Qt::DisplayRole)
        return stringList.at(index.row());
    else
        return QVariant();
}


QVariant MyStringListModel::headerData(int section, Qt::Orientation orientation, int role) const
{//頭部的顯示信息增加界面友好性
    if (role != Qt::DisplayRole)
        return QVariant();


    if (orientation == Qt::Horizontal)
        return QString("Column %1").arg(section);
    else
        return QString("Row %1").arg(section);
}

至此,我們創建的Model可以表示一個字符串列表,供Views來顯示。如果想要修改其內容,需要再添加其它函數實現(如flags, setData),并借助Delegates來實現和用戶的特定交互方式(editline,還是combo box等),可參見后面。

2.Views

不同的view都完整實現了各自的功能:QListView把Model的數據顯示為一個列表,QTableView把Model的數據以table的形式表現,QTreeView 用具有層次結構的列表來顯示model中的數據。這些類都基于QAbstractItemView抽象基類,盡管這些類都是現成的,完整的進行了實現,但它們都可以用于子類化以便滿足定制需求。

在model/view架構中,view從model中獲得數據項然后顯示給用戶。數據顯示的方式不必與model提供的表示方式相同,可以與底層存儲數據項的數據結構完全不同。這種內容與顯式的分離是通過由QAbstractItemModel提供的標準模型接口,由QAsbstractItemview提供的標準視圖接口共同實現的。

普遍使用model index來表示數據項。view負責管理從model中讀取的數據的外觀布局。它們自己可以去渲染每個數據項,也可以利用delegate來既處理渲染又進行編輯。

除了顯示數據,views也處理數據項的導航,參與有關于數據項選擇的部分功能。view也實現一些基本的用戶接口特性,如上下文菜單與拖拽功能。view也為數據項提供了缺省的編程功能,也可搭配delegate實現更為特殊的定制編輯的需求。一個view創建時必不需要model,但在它能顯示一些真正有用的信息之前,必須提供一個model。view通過使用selections來跟蹤用戶選擇的數據項。每個view可以維護單獨使用的selections,也可以在多個views之間共享(例如在QListView中選擇一項,同時也在使用同一個selections同一model的QTreeView中顯示出來)。

有些views,如QTableView和QTreeView,除數據項之外也可顯示標題(Headers),標題部分通過一個view來實現,QHeaderView。標題與view一樣總是從相同的model中獲取數據。從 model中獲取數據的函數是QabstractItemModel::headerDate(),一般總是以表單的形式中顯示標題信息。可以從QHeaderView子類化,以實現更為復雜的定制化需求。

(1)使用model

a.前面的例子中創建過一個string list model,這里給它設置一些數據,再創建一個view把model中的內容展示出來:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // Unindented for quoting purposes:
    QStringList numbers;
    numbers << "One" << "Two" << "Three" << "Four" << "Five";


    QAbstractItemModel *model = new StringListModel(numbers);
    //要注意的是,這里把StringListModel作為一個QAbstractItemModel來使用。這樣我們就可以
    //使用model中的抽象接口,而且如果將來我們用別的model代替了當前這個model,這些代碼也會照樣工作。
    //QListView提供的列表視圖足以滿足當前這個model的需要了。
    QListView *view = new QListView;
    view->setModel(model);
    view->show();
    return app.exec();
}

view會渲染model中的內容,通過model的接口來訪問它的數據。當用戶試圖編輯數據項時,view會使用缺省的delegate來提供一個編輯構件。

b.一個model,多個views

為多個views提供相同的model是非常簡單的事情,只要為每個view設置相同的model。

QTableView *firstTableView = new QTableView;
QTableView *secondTableView = new QTableView;

firstTableView->setModel(model);
secondTableView->setModel(model);

在model/view架構中信號、槽機制的使用意味著model中發生的改變會傳遞中聯結的所有view中,這保證了不管我們使用哪個view,訪問的都是同樣的一份數據。

c.多個views之間共享選擇

接著上邊的例子,我們可以這樣:

secondTableView->setSelectionModel(firstTableView->selectionModel());

現在所有views都在同樣的選擇模型上操作,數據與選擇項都保持同步,在firstTableView上選擇的項,在secondTableView上也會高亮出來。

(2)使用選擇項

另外,在views中還可使用選擇項,設定選擇哪些數據,以及更新和讀取選擇的狀態等。這里省略,可參見QItemSelection。

3.Delegates

與MVC模式不同,model/view結構沒有用于與用戶交互的完全獨立的組件。一般來講, view負責把數據展示給用戶,也處理用戶的輸入。為了獲得更多的靈活性,交互通過delegagte執行。

Delegate既提供輸入功能又負責渲染view中的每個數據項。 控制delegates的標準接口在QAbstractItemDelegate類中定義,Delegates通過實現paint()和sizeHint()以達到渲染內容的目的。然而,簡單的基于widget的delegates,可以從QItemDelegate子類化,而不是QAbstractItemDelegate,這樣可以使用它提供的上述函數的缺省實現。delegate可以使用widget來處理編輯過程,也可以直接對事件進行處理。

Qt提供的標準views都使用QItemDelegate的實例來提供編輯功能。它以普通的風格來為每個標準view渲染數據項。這些標準的views包括:QListView,QTableView,QTreeView。所有標準的角色都通過標準views包含的缺省delegate進行處理。一個view使用的delegate可以用itemDelegate()函數取得,而setItemDelegate() 函數可以安裝一個定制delegate。

實現自定制的delegate

在前面,我們已經建立了一個基于字符串的QStringListModel,我們這里用自己定義的delegate來控制每一項的編輯和渲染。我們建立了一個list view來顯示model的內容,用我們定制的delegate來編輯和顯示,這個delegate使用QLineEdit來提供編輯和顯示的功能(當然我們也可用自己定義的窗口組件,這里使用編輯器有點誤導人,好像delegate只能編輯似的,實際我們可以將delegate看作一個任意的窗口部件,其輸入輸出就是model中的數據,而views實際就是對delegate以列表,樹形結構等方式組織起來,更進一步用什么方式展示數據,由delegate決定)。

類聲明

我們繼承QItemDelegate,這樣可以利用它缺省實現的顯示功能。當然我們必需提供函數來管理用于編輯的widget:

class LineEditDelegate : public QItemDelegate
{
    Q_OBJECT


    public:
        LineEditDelegate(QObject *parent = 0);
        QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;//提供編輯器
        void setEditorData(QWidget *editor, const QModelIndex &index) const;//用編輯器渲染model的data
        void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;//向model提交修改的data。
        void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const;//更新編輯器幾何布局
};

需要注意的是,當一個delegate創建時,不需要安裝一個widget,只有在真正需要時才創建這個用于編輯的widget。

類實現

當List view需要提供一個編輯器時,它要求delegate提供一個widget編輯器,修改當前的數據項。createEditor()函數用于創建那個編輯器。

QWidget *LineEditDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/* option */, const QModelIndex &/* index */) const
{
    QLineEdit *editor = new QLineEdit(parent);
    return editor;
}

我們不需要跟蹤這個widget的指針,因為view會在不需要時銷毀這個widget。我們也可以根據不同的model index來創建不同的編輯器,比如,我們有一列整數,一列字符串,我們可以根據哪種列被編輯來創建一個QSpinBox或是QLineEdit。

delegate必需能夠把model中的數據拷貝到編輯器中,以起到渲染的作用。這需要我們實現setEditorData()函數。

void LineEditDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    QString text = index.model()->data(index, Qt::DisplayRole).toString();


    QLineEdit *line = static_cast(editor);
    line->setText(text);
}

這樣,數據便以QLineEdit的方式被渲染出來。

delegate必需能夠把在編輯器中修改好的數據提交給model。這需要我們實現另外一個函數setModelData()。

void LineEditDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QLineEdit *line = static_cast(editor);
    QString text = line->text();
    model->setData(index, text);
}

標準的QItemDelegate類當它完成編輯時會發射closeEditor()信號來通知view。view保證編輯器widget關閉與銷毀。本例中我們只提供簡單的編輯功能,因此不需要發送個信號。

delegate負責管理編輯器的幾何布局。這些幾何布局信息在編輯創建時或view的尺寸位置發生改變時,都應當被提供。view通過一個view option可以提供這些必要的信息。

void LineEditDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
    editor->setGeometry(option.rect);
}
  1. 編輯提示

    編輯完成后,delegate會給別的組件提供有關于編輯處理結果的提示,也提供用于后續編輯操作的一些提示。這可以通過發射帶有某種hint的closeEditor()信號完成。這些信號會被安裝在line edit上的缺省的QItemDelegate事件過濾器捕獲。對這個缺省的事件過濾來講,當用戶按下回車鍵,delegate會對model中的數據進行提交,并關閉spin box。 我們可以安裝自己的事件過濾器以迎合我們的需要,例如,我們可以發射帶有EditNextItem hint的 closeEditor()信號來實現自動開始編輯view中的下一項。

  2. 繼續完善model以支持delegate

    delegate會在創建編輯器之前檢查數據項是否是可編輯的。model必須得讓delegate知道它的數據項是可編輯的。這可以通過為每一個數據項返回一個正確的標記得到,在本例中,我們假設所有的數據項都是可編輯可選擇的。所以需要為MyStringListModel實現flags函數。

    Qt::ItemFlags MyStringListModel::flags(const QModelIndex &index) const
    {
        if (!index.isValid())
            return Qt::ItemIsEnabled;
    
    
        return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
    }
    

    model不必知道delegate執行怎樣實際的編輯處理過程,但需提供給delegate一個方法,delegate會使用它對model中的數據進行設置。這個特殊的函數就是setData()。

    bool MyStringListModel::setData(const QModelIndex &index, const QVariant &value, int role)
    {
        if (index.isValid() && role == Qt::EditRole) {
    
    
            stringList.replace(index.row(), value.toString());
            emit dataChanged(index, index);
            return true;
        }
        return false;
    }
    

    這里當數據被設置后,model必須得讓views知道一些數據發生了變化,這里通過發射一個dataChanged() 信號實現。因為只有一個數據項發生了變化,因此在信號中說明的變化范圍只限于一個model index。

    另外,還可實現插入行和刪除行的功能,需要實現兩個函數,并在view處做相應處理,這里不細述。

    bool MyStringListModel::insertRows(int position, int rows, const QModelIndex &parent)
    {//beginInsertRows()通知其他組件行數將會改變。endInsertRows()對操作進行確認與通知。返回true表示成功。
        beginInsertRows(QModelIndex(), position, position+rows-1);
    
    
        for (int row = 0; row < rows; ++row) {
            stringList.insert(position, "");
        }
    
    
        endInsertRows();
        return true;
    }
    
    
    bool MyStringListModel::removeRows(int position, int rows, const QModelIndex &parent)
    {
        beginRemoveRows(QModelIndex(), position, position+rows-1);
    
    
        for (int row = 0; row < rows; ++row) {
            stringList.removeAt(position);
        }
    
    
        endRemoveRows();
        return true;
    }
    

繪制系統

Qt的繪制系統主要由三部分組成,QPainter, QPaintDevice, QPaintEngine。

  • QPainter 是一個繪制接口類,提供繪制各種面向用戶的命令;
  • QPaintDevice 是對被QPainter繪制的設備(目的地)進行抽象形成的2維空間,相當于畫布;
  • 而QPaintEngine 是介于兩者之間的基本繪制命令的具體實現,被兩者在內部調用,它根據被繪制的設備的不同而不同。

由于這個結構,我們繪制時就不用關心具體的設備(QPaintDevice),直接和QPainter打交道即可。注意對于Windows平臺來說,當繪制目標是一個widget的時候,QPainter只能在 paintEvent() 里面或者由paintEvent()導致調用的函數里面使用。另外,Qt提供了對OpenGL的支持,通過和QWidget類似的方式,使用OpenGL的功能。

1.Matrix, Coordinate, View port & window

默認情況下,QPainter 使用的是 當前 device 的坐標系,坐標原點是左上角,x軸向右遞增,y軸向下遞增,坐標單位,對于基于像素的設備是1個像素,對于打印機是1/72英寸。

但是QPainter 對于坐標系變換提供了很好的支持,主要有如下的一些坐標系變換:

  • 旋轉
  • 縮放
  • 平移
  • shearing

用 scale() 來縮放, rotate() 用來對坐標系進行順時針旋轉, translate() 對坐標系執行平移操作。也可以通過函數 shear() 對坐標系執行扭曲。類似對矩陣執行雅克比切變,讓 坐標系的 x 和 y 不再是正交的向量。這里提到的變換都是作用在 worldTransform()的矩陣上。 還有一個矩陣 deviceTransform 用來把邏輯坐標變換到設備的坐標。

當用QPainter執行繪制的時候,我們指定的頂點坐標都是邏輯坐標,這個邏輯坐標最終會被轉換成設備的物理坐標,從邏輯坐標到物理坐標的轉換,是通過矩陣combinedTransform()執行的,這個矩陣,結合了 viewport() , window(), 和 worldTransform()。 其中 viewport()代表的是物理坐標系中的任意的一個矩形區域,而window()是以邏輯坐標的形式描述viewport()指定的同一個矩形。其中worldTransform() 就等于變換矩陣。

2.繪制內容

對于QPainter來說,內部有一個狀態堆棧,任何時候都可以通過調用 save() 和 restore() 對QPainter的內部狀態(如旋轉角度等)執行 進棧保存和壓棧還原的操作。

QPainter 提供了大部分基本二維幾何元的繪制命令,可以繪制如:QPoint, QLine, QRect, QRegion, QPolygon等表示的圖形,以及一些復雜的圖形可通過QPainterPath來進行。QPainterPath實際是一個各種基本繪制操作的容器,將復雜的繪制操作存于QPainterPath中,然后通過 QPainter::drawPath()一次性繪制出來。

另外,QPainter還可以繪制文字,以及pixmap(對圖片的像素表示,不能直接顯示需要轉成相應的圖片格式顯示)。

3.填充

填充使用QBrush來完成,可以指定填充的顏色和風格。填充的顏色用QColor表示,風格 Qt::BrushStyle 枚舉列出。還可通過 QGradient 來自行指定填充的梯度,以及通過QPixmap自行指定填充紋理。

4.創建繪制設備

QPaintDevice是繪制設備的基類。QPainter可以在任何QPaintDevice子類對象上進行繪制。目前Qt實現的QPaintDevice包括:QWidget, QImage, QPixmap, QGLWidget, QGLPixelBuffer, QPicture 和 QPrinter和子類等。

如果添加自己的繪制后端:

  • 我們需要繼承QPaintDevice,重新實現虛函數:QPaintDevice::paintEngine()以確定QPaintDevice使用哪個engine。
  • 我們還需繼承QPaintEngine來實現這個engine,以便能夠在添加的設備上進行繪制。

5.讀寫圖片文件

Qt提供了四種類用來處理圖片數據:QImage, QPixmap, QBitmap以及QPicture。

  • QImage對I/O的操作進行了優化,用于直接對像素進行訪問和操作。
  • QPixmap對顯示圖片到屏幕上進行了優化。
  • QBitmap是QPixmap的子類,保證了色深為1。
  • QPicture是一個繪制設備,用來記錄和回放QPainter的操作。

對圖片操作最常用的類就是QImage和QPixmap。可以通過其構造函數,或者load、save函數。另外Qt也提供了QImageReader和 QImageWriter可以對圖片處理提供更多更方便的控制。

QMovie用來顯示動畫,其內部使用了QImageReader。QImageWriter和QImageReader依賴QImageIOHandler,QImageIOHandler為Qt提供了操作所有格式圖片的一些通用接口。QImageWriter和QImageReader內部就使用QImageIOHandler為Qt添加不同圖片格式的支持。

Qt里支持了一些格式的圖片,可通過 QImageReader::supportedImageFormats() 和 QImageWriter::supportedImageFormats()來查詢。若為Qt添加新圖片格式的支持,通過插件實現。繼承QImageIOHandler以及創建用于創建handler的QImageIOPlugin 對象后,就可以用QImageWriter和QImageReader來操作這個新格式的圖片了。

另外Qt還支持靜態SVG圖片,見QtSvg相關文檔。

6.風格化

Qt的內建控件一般都使用QStyle來進行繪制。QStyle是風格的基類。每種風格都代表一種gui的顯示特性,例如(windows下的,linux下的),如果定義自己顯示個性的風格,那么可以通過插件機制來實現。

大多數函數繪制風格元素需要四個參數:

  • 一個表示哪種圖形元素的枚舉。
  • 一個QStyleOption描述怎樣以及向哪來提交元素。
  • 一個QPainter對象用于繪制元素。
  • 一個QWidget對象,繪制的動作將在其上進行(可選)。

QStylePainter 繼承自QPainter,可以方便地利用QStyle進行風格繪制。

7.選擇繪制后端

Qt4.5開始,我們可以選擇替換用于widgets,pixmaps,和無屏雙緩沖的engines和devices。各個系統默認的后端是:

Windows系統:Software Rasterizer
X11系統:X11
Mac OS X系統:CoreGraphics
Embedded系統:Software Rasterizer

我們可以通過啟動程序時指定"-graphicssystem raster"來告訴Qt使用rasterizer軟件作為程序的后端。rasterizer軟件對所有的平臺都支持的很好。如:

$analogclock -graphicssystem raster

也可以使用"-graphicssystem opengl",來指定使用OpenGL繪制。當前,這個引擎處于實驗階段,可能不能正確繪制所有內容。

Qt也支持"-graphicssystem raster|opengl"配置,這樣所有的應用程序將會使用相應的圖形系統。

其它

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374