天天看點

Qt中的核心技術

這裡簡單介紹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。

(*)事件機制

=======================

參考:http://blog.csdn.net/wangqis/article/details/4547997

事件機制主要用于對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"指定通過哪種驅動顯示。例如:

啟動服務後,

(a)通過環境變量:

#export QWS_DISPLAY="VNC:0"

#myApplication&

(b)通過指令行選項:

#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用于建立插件:

高層的API:用于擴充Qt本身,比如自定義的資料庫驅動,文本解碼插件,風格插件等。

低層的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);

QPointer<QObject>p=child;

delete parent;

if(p.isNull()){

p->...

}

}

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

(*)Qt的Model/View程式設計模式

=======================

Qt 4使用model/view結構來管理資料與表示層的關系。這種結構将顯示與資料分離,給開發人員帶來更大的彈性來定制資料項的表示。Model-View-Controller(MVC), 是從Smalltalk發展而來的一種設計模式,常被用于建構使用者界面。MVC 由三種對象組成。Model是應用程式對象,View是它的螢幕表示,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。model,與資料源通訊,并提供接口給結構中的别的元件使用。通訊的性質依賴于資料源的種類與model實作的方式; view,從model擷取model indexes,通過model indexes,view可以從model資料源中擷取資料并組織; 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 &, const QModelIndex &) 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<QLineEdit*>(editor);

line->setText(text);

}

這樣,資料便以QLineEdit的方式被渲染出來。

delegate必需能夠把在編輯器中修改好的資料送出給model。這需要我們實作另外一個函數setModelData()。

void LineEditDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const

{

QLineEdit *line = static_cast<QLineEdit*>(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 &) const

{

editor->setGeometry(option.rect);

}

編輯提示

編輯完成後,delegate會給别的元件提供有關于編輯處理結果的提示,也提供用于後續編輯操作的一些提示。這可以通過發射帶有某種hint的closeEditor()信号完成。這些信号會被安裝在line edit上的預設的QItemDelegate事件過濾器捕獲。對這個預設的事件過濾來講,當使用者按下Enter鍵,delegate會對model中的資料進行送出,并關閉spin box。 我們可以安裝自己的事件過濾器以迎合我們的需要,例如,我們可以發射帶有EditNextItem hint的 closeEditor()信号來實作自動開始編輯view中的下一項。

繼續完善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"配置,這樣所有的應用程式将會使用相應的圖形系統。

作者:QuietHeart

Email:[email protected]

日期:2013年10月15日

繼續閱讀