Qt Quick綜合例項之檔案檢視器
如果你基於Qt SDK 5.3.1來建立一個Qt Quick App專案,專案模板為你準備的main.qml文件的根元素是ApplicationWindow或Window。這次我們就以ApplicationWindow為例,圍繞著它實現一個綜合例項:檔案檢視器。通過檔案檢視器的實現,我們來再次領略一下Qt Quick的犀利。
本例項將會用到下列特性:
- ApplicationWindow
- MenuBar
- ToolBar、ToolButton
- Action
- StatusBar
- MediaPlayer
- Image
- XMLHttpRequest
- ColorDialog
- FileDialog
- TextArea
- 動態建立QML元件
- 多介面切換
我們之前的文章都是就某一個主題來展開的,這次要出個大招。先看點兒圖。
檔案檢視器效果
圖1,初始狀態
圖1 初始狀態
圖2 是瀏覽文字檔案的效果:
圖2 瀏覽文字檔案
圖3是瀏覽圖片:
圖3 瀏覽圖片
圖4是播放視訊:
圖4 播放視訊
圖5是播放音樂:
圖5 播放音樂
好啦,看程式碼前,先大概介紹下用到的Qt Quick元素。
Image、動態建立元件、介面切換這些沒什麼好說的,我之前的文章中已經涉及到。只講新的了。
ApplicationWindow
ApplicationWindow,類似Qt C++中的QMainWindow,請對照著理解。
ApplicationWindow有選單欄(menuBar屬性)、工具欄(toolBar屬性)、狀態列(statusBar屬性)。咦,沒有centralWidget哈,別急,有的,就是ContentItem,就是那個你不與任何屬性繫結的Item。例如:
ApplicationWindow{ ... Rectangle{ id: centralView; anchors.fill: parent; color: "red" ; }}
上面程式碼中的centralView就對應QMainWindow的centralWidget了。看起來沒有DockWidgets……兄弟,別要求太高了,你想要麼,真的想要麼,你真的確定你想要麼?好吧,自己實現就好了,QML裡很多元素的哈。
MenuBar
MenuBar就是選單欄一長條區域了,它幹兩件事:
- 維護一個Menu列表(menus屬性)
- 繪製選單欄背景色(style屬性)
Menu
看截圖中的“檔案”、“設定”、“幫助”三個選單,點一下彈出一個子選單,Menu就代表檔案這個選單和它下面的子選單。
列表屬性items指向Menu的子選單項,它的型別是list<Object>,是預設屬性。
title屬性是string型別,你看到的“檔案”、“設定”等字樣就是它指定的。
有這兩個屬性,Menu就可以開始幹活了。看Qt幫助裡的示例程式碼片段:
Menu { title: "Edit" MenuItem { text: "Cut" shortcut: "Ctrl+X" onTriggered: ... } MenuItem { text: "Copy" shortcut: "Ctrl+C" onTriggered: ... } MenuItem { text: "Paste" shortcut: "Ctrl+V" onTriggered: ... } MenuSeparator { } Menu { title: "More Stuff" MenuItem { text: "Do Nothing" } }}
MenuItem
Menu的孩子啊,最受待見的就是MenuItem了。
MenuItem代表一個具體的選單項,點一下就幹一件事情的角色。你可以實現onTriggered訊號處理響應使用者對它的選擇。
MenuItem的text屬性指定選單文字,iconSource屬性指定選單圖示。
Action屬性很強大哦,MenuItem的text、iconSource、trigger訊號等實現的效果,都可以通過Action來實現,這兩種方式是等同的。咱們來看Action。
Action
Action類有text、iconSource等屬性,還有toggled、triggered兩個訊號。這些在MenuItem裡有對應的屬性,不必多說了。
使用Action的一大好處是,你可以給它指定一個id(比如叫open),然後在使用ToolButton構造ToolBar時指定ToolBatton的action屬性為之前定義的id為open的那個action,這樣工具欄的按鈕就和選單關聯起來了。
好啦,總結一下,實現選單欄的典型程式碼結構是醬紫的:
MenuBar { Menu{ title:"檔案"; MenuItem{ text:"開啟"; iconSource: "res/ic_open.png"; onTriggered:{ ... } } MenuItem{ action: Action { text:"儲存"; iconSource: "res/ic_save.png"; onTriggered:{ ... } } } }}
如你所見,我使用了兩種構造MenuItem的方式,一種用到了Action,一種沒有。ToolBar
ToolBar就是工具欄對應的類,它只有一個屬性,contentItem,型別為Item。一般我們可以將一個Row或者RowLayout物件賦值給contentItem,而Row或RowLayout則管理一組ToolButton來作為工具欄上的按鈕。
ToolButton
ToolButton是Button的派生類,專為ToolBar而生,一般情況下定義ToolButton物件時只需要指定其iconSource屬性即可。例如:
ToolButton { iconSource: "res/ic_open.png";}
還有一種方式是將一個已定義好的Action物件關聯到ToolButton物件上。例如:ToolButton{ action: openAction;}
這樣的話,ToolButton會使用Action定義的iconSource或iconName作為其圖示。好啦,構造工具欄的典型程式碼結構如下:
ToolBar{ RowLayout { ToolButton{ action: textAction; } ToolButton{ action: imageAction; } ToolButton{ iconSource: "res/exit.png"; onClicked:{ ... } } }}
狀態列
ApplicationWindow的屬性statusBar代表狀態列,其型別為Item,你可以將任意的Item賦值給它,可以隨心所欲構建你妖嬈多姿的狀態列。比如這樣:
ApplicationWindow{ statusBar: Text { text: "status bar"; color: "blue"; }}
MediaPlayer
Qt Quick裡,播放視訊、音訊檔案都直接使用MediaPlayer類,它是萬能的,不要說你萬萬沒想到哈。
對於音樂,最簡單,設定source屬性,呼叫play()方法,就可以了。例如:
ApplicationWindow{ ... MediaPlayer{ id: player; source: "xxx.mp3"; } Component.onCompleted:{ player.play(); }}
對於視訊,MediaPlayer還得尋求場外幫助,求助物件就是它的好基友:VideoOutput !簡單的示例程式碼:ApplicationWindow{ ... MediaPlayer { id: player; source: "test.ts"; } VideoOutput { anchors.fill: parent; source: player; } Component.onCompleted: player.play(); }
哦,黃小琥唱過的歌:沒那麼簡單。是的,實際做專案比較複雜,你還要關注各種播放狀態,要seek,要pause,要處理錯誤,簡直煩死人了。請移步到Qt幫助裡瞭解詳情。XMLHttpRequest
在Qt Quick裡,要訪問網路腫麼辦泥?答案是:XMLHttpRequest !
不要被它傲嬌的外表迷惑,以為它只接受XML文件,其實,它什麼都能處理,txt、html、json、binary……它溫柔堅定強悍無比。
本例項用它來載入本地的文字檔案,耶,這樣都可以哈。誰叫Qt Quick不提供直接訪問本地檔案的類庫呢!我可不想跑到C++裡用QFile、QTextStream這對黃金搭檔。
XMLHttpRequest的文件,Qt幫助里語焉不詳,只有一個示例,請看這裡:
TextArea
這有什麼好講的,看Qt幫助吧。只提一點:
TextArea自動處理翻頁按鍵、上下鍵、滑鼠中鍵,正確的滾動文字;而TextEdit,抱歉,我是來打醬油的。
標準對話方塊
Qt Quick提供了很多標準對話方塊,比如FileDialog用來選擇檔案或資料夾,ColorDialog用來選擇顏色,MessageDialog用來顯示一些提示資訊。這些我們例項中用到了,參考Qt幫助吧。
我只說點兒經驗。
我一開始使用qmlscene來載入main.qml,出來的介面比較正常,工具欄的圖示、選單項前也有圖示。可是當我建立了一個Qt Quick App,靈異事件發生了:
選單項前面沒有圖示了……
工具欄圖示好大好大……
顏色對話方塊、訊息框,點選右上角的關閉按鈕,收不到rejected訊號啊……
查了老半天,猛回頭,警世鐘響起,我了悟了。原來是醬紫的:
Qt Quick提供了這些標準對話方塊的預設實現,如果應用執行的平臺沒有可用的,就用這些預設實現。那在Windows上,如果你的main()函式,使用QGuiApplication而非QApplication,就會用到Qt Quick實現的版本,一切都變了模樣
資源管理
Qt SDK 5.3之後,Qt Creator建立的Qt Quick App專案,就為我們建立了一個qrc檔案,把main.qml扔裡啦。我在例項中也把很多圖示扔裡了。
使用qrc來管理資源,這是跨平臺的,推薦使用。
我還自己畫了些圖示,真費勁,弄得也不好看。不過應用的大眼睛圖示,看起來還像那麼一回事兒。
原始碼
前戲太長,也許你已經失去了興趣。好吧,G點來咧。
QML程式碼
所有QML程式碼都在這裡了,竟然有450行啊親。
import QtQuick 2.2import QtQuick.Window 2.1import QtQuick.Controls 1.2import QtQuick.Controls.Styles 1.2import QtQuick.Layouts 1.1import QtQuick.Dialogs 1.1import QtMultimedia 5.0ApplicationWindow { visible: true width: 480 height: 360; color: "black"; title: "檔案檢視器"; id: root; property var aboutDlg: null; property var colorDlg: null; property color textColor: "green"; property color textBackgroundColor: "black"; menuBar: MenuBar{ Menu { title: "檔案"; MenuItem{ iconSource: "res/txtFile.png"; action: Action{ id: textAction; iconSource: "res/txtFile.png"; text: "文字檔案"; onTriggered: { fileDialog.selectedNameFilter = fileDialog.nameFilters[0]; fileDialog.open(); } tooltip: "開啟txt等文字檔案"; } } MenuItem{ action: Action { id: imageAction; text: "圖片"; iconSource: "res/imageFile.png"; onTriggered: { fileDialog.selectedNameFilter = fileDialog.nameFilters[1]; fileDialog.open(); } tooltip: "開啟jpg等格式的圖片"; } } MenuItem{ action: Action { id: videoAction; iconSource: "res/videoFile.png"; text: "視訊"; onTriggered: { fileDialog.selectedNameFilter = fileDialog.nameFilters[2]; fileDialog.open(); } tooltip: "開啟TS、MKV、MP4等格式的檔案"; } } MenuItem{ action: Action { id: audioAction; iconSource: "res/audioFile.png"; text: "音樂"; onTriggered: { fileDialog.selectedNameFilter = fileDialog.nameFilters[3]; fileDialog.open(); } tooltip: "開啟mp3、wma等格式的檔案"; } } MenuItem{ text: "退出"; onTriggered: Qt.quit(); } } Menu { title: "設定"; MenuItem { action: Action { id: textColorAction; iconSource: "res/ic_textcolor.png"; text: "文字顏色"; onTriggered: root.selectColor(root.onTextColorSelected); } } MenuItem { action: Action{ id: backgroundColorAction; iconSource: "res/ic_bkgndcolor.png"; text: "文字背景色"; onTriggered: root.selectColor(root.onTextBackgroundColorSelected); } } MenuItem { action: Action{ id: fontSizeAddAction; iconSource: "res/ic_fontsize2.png"; text: "增大字型"; onTriggered: textView.font.pointSize += 1; } } MenuItem { action: Action{ id: fontSizeMinusAction; iconSource: "res/ic_fontsize1.png"; text: "減小字型"; onTriggered: textView.font.pointSize -= 1; } } } Menu { title: "幫助"; MenuItem{ text: "關於"; onTriggered: root.showAbout(); } MenuItem{ text: "訪問作者部落格"; onTriggered: Qt.openUrlExternally("http://blog.csdn.net/foruok"); } } } toolBar: ToolBar{ RowLayout { ToolButton{ action: textAction; } ToolButton{ action: imageAction; } ToolButton{ action: videoAction; } ToolButton{ action: audioAction; } ToolButton{ action: textColorAction; } ToolButton { action: backgroundColorAction; } ToolButton { action: fontSizeAddAction; } ToolButton { action: fontSizeMinusAction; } } } statusBar: Rectangle { color: "lightgray"; implicitHeight: 30; width: parent.width; property alias text: status.text; Text { id: status; anchors.fill: parent; anchors.margins: 4; font.pointSize: 12; } } Item { id: centralView; anchors.fill: parent; visible: true; property var current: null; BusyIndicator { id: busy; anchors.centerIn: parent; running: false; z: 3; } Image { id: imageViewer; anchors.fill: parent; visible: false; asynchronous: true; fillMode: Image.PreserveAspectFit; onStatusChanged: { if (status === Image.Loading) { centralView.busy.running = true; } else if(status === Image.Ready){ centralView.busy.running = false; } else if(status === Image.Error){ centralView.busy.running = false; centralView.statusBar.text = "圖片無法顯示"; } } } TextArea { id: textView; anchors.fill: parent; readOnly: true; visible: false; wrapMode: TextEdit.WordWrap; font.pointSize: 12; style: TextAreaStyle{ backgroundColor: root.textBackgroundColor; textColor: root.textColor; selectionColor: "steelblue"; selectedTextColor: "#a00000"; } property var xmlhttp: null; function onReadyStateChanged(){ if(xmlhttp.readyState == 4){ text = xmlhttp.responseText; xmlhttp.abort(); } } function loadText(fileUrl){ if(xmlhttp == null){ xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = onReadyStateChanged; } if(xmlhttp.readyState == 0){ xmlhttp.open("GET", fileUrl); xmlhttp.send(null); } } } VideoOutput { id: videoOutput; anchors.fill: parent; visible: false; source: player; onVisibleChanged: { playerState.visible = visible; } MouseArea { anchors.fill: parent; onClicked: { switch(player.playbackState){ case MediaPlayer.PausedState: case MediaPlayer.StoppedState: player.play(); break; case MediaPlayer.PlayingState: player.pause(); break; } } } } Rectangle { id: playerState; color: "gray"; radius: 16; opacity: 0.8; visible: false; z: 2; implicitHeight: 80; implicitWidth: 200; anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom; anchors.bottomMargin: 16; Column { anchors.fill: parent; anchors.leftMargin: 12; anchors.rightMargin: 12; anchors.topMargin: 6; anchors.bottomMargin: 6; spacing: 4; Text { id: state; font.pointSize: 14; color: "blue"; } Text { id: progress; font.pointSize: 12; color: "white"; } } } MediaPlayer { id: player; property var utilDate: new Date(); function msecs2String(msecs){ utilDate.setTime(msecs); return Qt.formatTime(utilDate, "mm:ss"); } property var sDuration; onPositionChanged: { progress.text = msecs2String(position) + sDuration; } onDurationChanged: { sDuration = " / " + msecs2String(duration); } onPlaybackStateChanged: { switch(playbackState){ case MediaPlayer.PlayingState: state.text = "播放中"; break; case MediaPlayer.PausedState: state.text = "已暫停"; break; case MediaPlayer.StoppedState: state.text = "停止"; break; } } onStatusChanged: { switch(status){ case MediaPlayer.Loading: case MediaPlayer.Buffering: busy.running = true; break; case MediaPlayer.InvalidMedia: root.statusBar.text = "無法播放"; case MediaPlayer.Buffered: case MediaPlayer.Loaded: busy.running = false; break; } } } } function processFile(fileUrl, ext){ var i = 0; for(; i < fileDialog.nameFilters.length; i++){ if(fileDialog.nameFilters[i].search(ext) != -1) break; } switch(i){ case 0: //text file if(centralView.current != textView){ if(centralView.current != null){ centralView.current.visible = false; } textView.visible = true; centralView.current = textView; } textView.loadText(fileUrl); break; case 1: if(centralView.current != imageViewer){ if(centralView.current != null){ centralView.current.visible = false; } imageViewer.visible = true; centralView.current = imageViewer; } imageViewer.source = fileUrl; break; case 2: case 3: if(centralView.current != videoOutput){ if(centralView.current != null){ centralView.current.visible = false; } videoOutput.visible = true; centralView.current = videoOutput; } player.source = fileUrl; player.play(); break; default: statusBar.text = "抱歉,處理不了"; break; } } function showAbout(){ if(aboutDlg == null){ aboutDlg = Qt.createQmlObject( 'import QtQuick 2.2;import QtQuick.Dialogs 1.1;MessageDialog{icon: StandardIcon.Information;title: "關於";\ntext: "僅僅是個示例撒";\nstandardButtons:StandardButton.Ok;}' , root, "aboutDlg"); aboutDlg.accepted.connect(onAboutDlgClosed); aboutDlg.rejected.connect(onAboutDlgClosed); aboutDlg.visible = true; } } function selectColor(func){ if(colorDlg == null){ colorDlg = Qt.createQmlObject( 'import QtQuick 2.2;import QtQuick.Dialogs 1.1;ColorDialog{}', root, "colorDlg"); colorDlg.accepted.connect(func); colorDlg.accepted.connect(onColorDlgClosed); colorDlg.rejected.connect(onColorDlgClosed); colorDlg.visible = true; } } function onAboutDlgClosed(){ aboutDlg.destroy(); aboutDlg = null; } function onColorDlgClosed(){ colorDlg.destroy(); colorDlg = null; } function onTextColorSelected(){ root.textColor = colorDlg.color; } function onTextBackgroundColorSelected(){ root.textBackgroundColor = colorDlg.color; } FileDialog { id: fileDialog; title: qsTr("Please choose an image file"); nameFilters: [ "Text Files (*.txt *.ini *.log *.c *.h *.java *.cpp *.html *.xml)", "Image Files (*.jpg *.png *.gif *.bmp *.ico)", "Video Files (*.ts *.mp4 *.avi *.flv *.mkv *.3gp)", "Audio Files (*.mp3 *.ogg *.wav *.wma *.ape *.ra)", "*.*" ]; onAccepted: { var filepath = new String(fileUrl); //remove file:/// if(Qt.platform.os == "windows"){ root.statusBar.text = filepath.slice(8); }else{ root.statusBar.text = filepath.slice(7); } var dot = filepath.lastIndexOf("."); var sep = filepath.lastIndexOf("/"); if(dot > sep){ var ext = filepath.substring(dot); root.processFile(fileUrl, ext.toLowerCase()); }else{ root.statusBar.text = "Not Supported!"; } } }}
C++程式碼
其實,我只對模板生成的C++程式碼改動了三行,一行include,一行QApplication,一行設定應用圖示。main.cpp如下:
#include <QApplication>#include <QQmlApplicationEngine>#include <QIcon>int main(int argc, char *argv[]){ QApplication app(argc, argv); app.setWindowIcon(QIcon(":/res/eye.png")); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); return app.exec();}
PRO檔案
有人喊,兄弟,這也要貼!你湊字數呢……你管,就放這裡了:
TEMPLATE = appQT += qml quick network multimedia widgetsSOURCES += main.cppRESOURCES += qml.qrc# Additional import path used to resolve QML modules in Qt Creator's code modelQML_IMPORT_PATH =# Default rules for deployment.include(deployment.pri)HEADERS +=
好啦,彪悍的人生不需要解釋,都扒光了,你自己仔細看吧。
回顧一下我的Qt Quick系列文章: